@openclaw/openshell-sandbox 2026.9.8 → 2026.10.1-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 +53 -97
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
- package/node_modules/@openclaw/fs-safe/README.md +70 -379
- package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
- package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
- 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/json-durable-queue-directory.js +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
- package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
- package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
- package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
- package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
- package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
- package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
- package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
- package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
- package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
- package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
- package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
- package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
- package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
- package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
- package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
- package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
- package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
- package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
- package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
- package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
- package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
- package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
- package/node_modules/@openclaw/fs-safe/package.json +10 -10
- 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/watch-alias.d.ts +0 -6
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
- package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
- package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
- package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
package/dist/index.js
CHANGED
|
@@ -9,7 +9,8 @@ import { GUEST_FILESYSTEM_PYTHON } from "@openclaw/fs-safe/guest";
|
|
|
9
9
|
import { createDeferred, formatPluginConfigIssue, mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
|
|
10
10
|
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
11
11
|
import { FsSafeError, canonicalPathFromExistingAncestor, extractErrorCode } from "openclaw/plugin-sdk/security-runtime";
|
|
12
|
-
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
|
+
import { asOptionalRecord, normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
13
|
+
import { safeParseJson } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
13
14
|
import { buildPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
14
15
|
import { MAX_TIMER_TIMEOUT_SECONDS } from "openclaw/plugin-sdk/number-runtime";
|
|
15
16
|
import { z } from "zod";
|
|
@@ -63,9 +64,9 @@ const DEFAULT_SOURCE = "openclaw";
|
|
|
63
64
|
const DEFAULT_REMOTE_WORKSPACE_DIR = "/sandbox";
|
|
64
65
|
const DEFAULT_REMOTE_AGENT_WORKSPACE_DIR = "/agent";
|
|
65
66
|
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
66
|
-
const
|
|
67
|
+
const OPEN_SHELL_MANAGED_REMOTE_PATH = /^\/(?:sandbox|agent)(?:\/|$)/;
|
|
67
68
|
const nonEmptyTrimmedString = (message) => z.string({ error: message }).trim().min(1, { error: message });
|
|
68
|
-
const openShellManagedRemotePath = (fieldName) => nonEmptyTrimmedString(`${fieldName} must be a non-empty string`).regex(
|
|
69
|
+
const openShellManagedRemotePath = (fieldName) => nonEmptyTrimmedString(`${fieldName} must be a non-empty string`).regex(OPEN_SHELL_MANAGED_REMOTE_PATH, { error: (issue) => String(issue.input).startsWith("/") ? `OpenShell ${fieldName} must stay under /sandbox or /agent` : `OpenShell ${fieldName} must be absolute` }).refine((value) => OPEN_SHELL_MANAGED_REMOTE_PATH.test(path.posix.normalize(value)), { error: `OpenShell ${fieldName} must stay under /sandbox or /agent` });
|
|
69
70
|
const openShellWorkspaceName = z.string({ error: "workspace must be a valid OpenShell workspace name" }).trim().min(1, { error: "workspace must be a valid OpenShell workspace name" }).max(19, { error: "workspace must be at most 19 characters" }).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { error: "workspace must contain lowercase alphanumeric characters or single hyphens and must not start or end with a hyphen" });
|
|
70
71
|
const OpenShellPluginConfigSchema = z.strictObject({
|
|
71
72
|
mode: z.enum(["mirror", "remote"], { error: "mode must be one of mirror, remote" }).optional(),
|
|
@@ -82,14 +83,9 @@ const OpenShellPluginConfigSchema = z.strictObject({
|
|
|
82
83
|
remoteAgentWorkspaceDir: openShellManagedRemotePath("remoteAgentWorkspaceDir").optional(),
|
|
83
84
|
timeoutSeconds: z.number({ error: `timeoutSeconds must be a number between 1 and ${MAX_TIMER_TIMEOUT_SECONDS}` }).min(1, { error: "timeoutSeconds must be a number >= 1" }).max(MAX_TIMER_TIMEOUT_SECONDS, { error: `timeoutSeconds must be a number <= ${MAX_TIMER_TIMEOUT_SECONDS}` }).optional()
|
|
84
85
|
});
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
function normalizeOpenShellRemotePath(value, fallback, fieldName = "remote path") {
|
|
89
|
-
const candidate = value ?? fallback;
|
|
90
|
-
const normalized = path.posix.normalize(candidate.trim() || fallback);
|
|
91
|
-
if (!normalized.startsWith("/")) throw new Error(`OpenShell ${fieldName} must be absolute: ${candidate}`);
|
|
92
|
-
if (!isManagedOpenShellRemotePath(normalized)) throw new Error(`OpenShell ${fieldName} must stay under ${OPEN_SHELL_MANAGED_REMOTE_ROOTS.join(" or ")}: ${candidate}`);
|
|
86
|
+
function normalizeOpenShellRemotePath(value) {
|
|
87
|
+
const normalized = path.posix.normalize(value);
|
|
88
|
+
if (!OPEN_SHELL_MANAGED_REMOTE_PATH.test(normalized)) throw new Error(`OpenShell remote path must stay under /sandbox or /agent: ${value}`);
|
|
93
89
|
return normalized;
|
|
94
90
|
}
|
|
95
91
|
function createOpenShellPluginConfigSchema() {
|
|
@@ -127,8 +123,8 @@ function resolveOpenShellPluginConfig(value) {
|
|
|
127
123
|
providers: [...new Set(cfg.providers ?? [])],
|
|
128
124
|
gpu: cfg.gpu ?? false,
|
|
129
125
|
autoProviders: cfg.autoProviders ?? true,
|
|
130
|
-
remoteWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteWorkspaceDir
|
|
131
|
-
remoteAgentWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteAgentWorkspaceDir
|
|
126
|
+
remoteWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteWorkspaceDir ?? DEFAULT_REMOTE_WORKSPACE_DIR),
|
|
127
|
+
remoteAgentWorkspaceDir: normalizeOpenShellRemotePath(cfg.remoteAgentWorkspaceDir ?? DEFAULT_REMOTE_AGENT_WORKSPACE_DIR),
|
|
132
128
|
timeoutMs: typeof cfg.timeoutSeconds === "number" ? Math.floor(cfg.timeoutSeconds * 1e3) : DEFAULT_TIMEOUT_MS
|
|
133
129
|
};
|
|
134
130
|
}
|
|
@@ -223,21 +219,17 @@ var OpenShellFsBridge = class {
|
|
|
223
219
|
}
|
|
224
220
|
async mkdirp(params) {
|
|
225
221
|
const target = this.resolveTarget(params);
|
|
226
|
-
const hostPath = target.hostPath;
|
|
227
222
|
this.ensureWritable(target, "create directories");
|
|
228
223
|
await assertLocalPathSafety({
|
|
229
224
|
target,
|
|
230
225
|
allowFinalSymlinkForUnlink: false
|
|
231
226
|
});
|
|
232
227
|
await this.backend.mkdirpRemotePath(target.containerPath, params.signal);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
target
|
|
236
|
-
});
|
|
228
|
+
const relativePath = relativeToRoot(target, target.hostPath);
|
|
229
|
+
if (relativePath) await (await root(target.mountHostRoot)).mkdir(relativePath);
|
|
237
230
|
}
|
|
238
231
|
async remove(params) {
|
|
239
232
|
const target = this.resolveTarget(params);
|
|
240
|
-
const hostPath = target.hostPath;
|
|
241
233
|
this.ensureWritable(target, "remove files", params.recursive);
|
|
242
234
|
await assertLocalPathSafety({
|
|
243
235
|
target,
|
|
@@ -250,15 +242,12 @@ var OpenShellFsBridge = class {
|
|
|
250
242
|
});
|
|
251
243
|
await removeLocalRootPath({
|
|
252
244
|
force: params.force,
|
|
253
|
-
hostPath,
|
|
254
245
|
recursive: params.recursive,
|
|
255
246
|
target
|
|
256
247
|
});
|
|
257
248
|
}
|
|
258
249
|
async rename(params) {
|
|
259
250
|
const { from, to } = this.resolveRenameTargets(params);
|
|
260
|
-
const fromHostPath = from.hostPath;
|
|
261
|
-
const toHostPath = to.hostPath;
|
|
262
251
|
await assertLocalPathSafety({
|
|
263
252
|
target: from,
|
|
264
253
|
allowFinalSymlinkForUnlink: true
|
|
@@ -267,20 +256,20 @@ var OpenShellFsBridge = class {
|
|
|
267
256
|
target: to,
|
|
268
257
|
allowFinalSymlinkForUnlink: false
|
|
269
258
|
});
|
|
270
|
-
await assertRenameSourceSupported(
|
|
259
|
+
await assertRenameSourceSupported(from.hostPath);
|
|
271
260
|
if (from.mountHostRoot !== to.mountHostRoot) throw new Error("OpenShell cross-root mirror renames require pinned fs-safe support");
|
|
272
261
|
await assertSameDeviceRenameSupported({
|
|
273
|
-
fromHostPath,
|
|
262
|
+
fromHostPath: from.hostPath,
|
|
274
263
|
root: from.mountHostRoot,
|
|
275
|
-
toHostPath
|
|
264
|
+
toHostPath: to.hostPath
|
|
276
265
|
});
|
|
277
266
|
await this.backend.renameRemotePath(from.containerPath, to.containerPath, params.signal);
|
|
278
|
-
await
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
});
|
|
267
|
+
const root$3 = await root(from.mountHostRoot);
|
|
268
|
+
const fromRelativePath = relativeToRoot(from, from.hostPath);
|
|
269
|
+
const toRelativePath = relativeToRoot(to, to.hostPath);
|
|
270
|
+
const parentPath = path.dirname(toRelativePath);
|
|
271
|
+
if (parentPath !== "." && parentPath !== "") await root$3.mkdir(parentPath);
|
|
272
|
+
await root$3.move(fromRelativePath, toRelativePath, { overwrite: true });
|
|
284
273
|
}
|
|
285
274
|
async stat(params) {
|
|
286
275
|
const target = this.resolveTarget(params);
|
|
@@ -396,18 +385,13 @@ function expectResolvedContainerTarget(target, input) {
|
|
|
396
385
|
if (!target) throw new Error(`Sandbox path escapes allowed mounts; cannot access: ${input}`);
|
|
397
386
|
return target;
|
|
398
387
|
}
|
|
399
|
-
async function mkdirLocalRootPath(params) {
|
|
400
|
-
const relativePath = relativeToRoot(params.target, params.hostPath);
|
|
401
|
-
if (!relativePath) return;
|
|
402
|
-
await (await root(params.target.mountHostRoot)).mkdir(relativePath);
|
|
403
|
-
}
|
|
404
388
|
async function removeLocalRootPath(params) {
|
|
405
|
-
const root$
|
|
406
|
-
const relativePath = relativeToRoot(params.target, params.hostPath);
|
|
389
|
+
const root$4 = await root(params.target.mountHostRoot);
|
|
390
|
+
const relativePath = relativeToRoot(params.target, params.target.hostPath);
|
|
407
391
|
try {
|
|
408
|
-
if (params.force === false) await fsPromises.lstat(params.hostPath);
|
|
409
|
-
const targets = params.recursive && !relativePath ? (await root$
|
|
410
|
-
for (const target of targets) await root$
|
|
392
|
+
if (params.force === false) await fsPromises.lstat(params.target.hostPath);
|
|
393
|
+
const targets = params.recursive && !relativePath ? (await root$4.list("")).map((name) => relativeToRoot(params.target, path.join(params.target.hostPath, name))) : [relativePath];
|
|
394
|
+
for (const target of targets) await root$4.remove(target, {
|
|
411
395
|
force: params.force !== false,
|
|
412
396
|
...params.recursive ? {
|
|
413
397
|
recursive: true,
|
|
@@ -421,18 +405,6 @@ async function removeLocalRootPath(params) {
|
|
|
421
405
|
throw err;
|
|
422
406
|
}
|
|
423
407
|
}
|
|
424
|
-
async function moveLocalRootPath(params) {
|
|
425
|
-
const root$4 = await root(params.from.mountHostRoot);
|
|
426
|
-
const fromRelativePath = relativeToRoot(params.from, params.fromHostPath);
|
|
427
|
-
const toRelativePath = relativeToRoot(params.to, params.toHostPath);
|
|
428
|
-
await mkdirParentPath(root$4, toRelativePath);
|
|
429
|
-
await root$4.move(fromRelativePath, toRelativePath, { overwrite: true });
|
|
430
|
-
}
|
|
431
|
-
async function mkdirParentPath(root, relativePath) {
|
|
432
|
-
const parentPath = path.dirname(relativePath);
|
|
433
|
-
if (parentPath === "." || parentPath === "") return;
|
|
434
|
-
await root.mkdir(parentPath);
|
|
435
|
-
}
|
|
436
408
|
function relativeToRoot(target, hostPath) {
|
|
437
409
|
const relativePath = path.relative(target.mountHostRoot, hostPath);
|
|
438
410
|
return relativePath ? `.${path.sep}${relativePath}` : "";
|
|
@@ -638,10 +610,23 @@ const ENSURE_OPEN_SHELL_REMOTE_REAL_DIRECTORY_SCRIPT = [
|
|
|
638
610
|
"done"
|
|
639
611
|
].join("\n");
|
|
640
612
|
function createOpenShellSandboxBackendFactory(params) {
|
|
641
|
-
return async (createParams) =>
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
613
|
+
return async (createParams) => {
|
|
614
|
+
if ((createParams.cfg.docker.binds?.length ?? 0) > 0) throw new Error("OpenShell sandbox backend does not support sandbox.docker.binds.");
|
|
615
|
+
const { sandboxName, legacyRuntimeAdopted } = resolveOpenShellSandboxName({
|
|
616
|
+
scopeKey: createParams.scopeKey,
|
|
617
|
+
registeredRuntimeIds: createParams.registeredRuntimeIds
|
|
618
|
+
});
|
|
619
|
+
return new OpenShellSandboxBackendImpl({
|
|
620
|
+
createParams,
|
|
621
|
+
execContext: {
|
|
622
|
+
config: params.pluginConfig,
|
|
623
|
+
sandboxName
|
|
624
|
+
},
|
|
625
|
+
legacyRuntimeAdopted,
|
|
626
|
+
remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
|
|
627
|
+
remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir
|
|
628
|
+
}).asHandle();
|
|
629
|
+
};
|
|
645
630
|
}
|
|
646
631
|
function createOpenShellSandboxBackendManager(params) {
|
|
647
632
|
return {
|
|
@@ -683,25 +668,6 @@ function createOpenShellSandboxBackendManager(params) {
|
|
|
683
668
|
}
|
|
684
669
|
};
|
|
685
670
|
}
|
|
686
|
-
async function createOpenShellSandboxBackend(params) {
|
|
687
|
-
if ((params.createParams.cfg.docker.binds?.length ?? 0) > 0) throw new Error("OpenShell sandbox backend does not support sandbox.docker.binds.");
|
|
688
|
-
const resolvedSandboxName = resolveOpenShellSandboxName({
|
|
689
|
-
scopeKey: params.createParams.scopeKey,
|
|
690
|
-
registeredRuntimeIds: params.createParams.registeredRuntimeIds
|
|
691
|
-
});
|
|
692
|
-
const sandboxName = resolvedSandboxName.sandboxName;
|
|
693
|
-
const execContext = {
|
|
694
|
-
config: params.pluginConfig,
|
|
695
|
-
sandboxName
|
|
696
|
-
};
|
|
697
|
-
return new OpenShellSandboxBackendImpl({
|
|
698
|
-
createParams: params.createParams,
|
|
699
|
-
execContext,
|
|
700
|
-
legacyRuntimeAdopted: resolvedSandboxName.legacyRuntimeAdopted,
|
|
701
|
-
remoteWorkspaceDir: params.pluginConfig.remoteWorkspaceDir,
|
|
702
|
-
remoteAgentWorkspaceDir: params.pluginConfig.remoteAgentWorkspaceDir
|
|
703
|
-
}).asHandle();
|
|
704
|
-
}
|
|
705
671
|
var OpenShellSandboxBackendImpl = class {
|
|
706
672
|
constructor(params) {
|
|
707
673
|
this.params = params;
|
|
@@ -1378,30 +1344,20 @@ function resolveOpenShellSandboxName(params) {
|
|
|
1378
1344
|
};
|
|
1379
1345
|
}
|
|
1380
1346
|
function parseOpenShellSandboxPhasePage(stdout, sandboxName) {
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
phase: record.phase
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
return { count: parsed.length };
|
|
1393
|
-
} catch {
|
|
1394
|
-
return;
|
|
1347
|
+
const parsed = safeParseJson(stdout);
|
|
1348
|
+
if (!Array.isArray(parsed)) return;
|
|
1349
|
+
for (const entry of parsed) {
|
|
1350
|
+
const record = asOptionalRecord(entry);
|
|
1351
|
+
if (record?.name === sandboxName && typeof record.phase === "string") return {
|
|
1352
|
+
count: parsed.length,
|
|
1353
|
+
phase: record.phase
|
|
1354
|
+
};
|
|
1395
1355
|
}
|
|
1356
|
+
return { count: parsed.length };
|
|
1396
1357
|
}
|
|
1397
1358
|
function parseOpenShellSandboxPhase(stdout) {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
if (typeof parsed !== "object" || parsed === null || !("phase" in parsed)) return;
|
|
1401
|
-
return typeof parsed.phase === "string" ? parsed.phase : void 0;
|
|
1402
|
-
} catch {
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1359
|
+
const parsed = asOptionalRecord(safeParseJson(stdout));
|
|
1360
|
+
return typeof parsed?.phase === "string" ? parsed.phase : void 0;
|
|
1405
1361
|
}
|
|
1406
1362
|
function resolveRemoteMaterializedSkillsWorkspaceDir(remoteWorkspaceDir) {
|
|
1407
1363
|
const root = remoteWorkspaceDir.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
|
|
@@ -2,6 +2,101 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.23.0 - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
|
|
9
|
+
- **BREAKING: Python bridge removed:** remove the Python bridge and its compatibility options; migrate to `configureFsSafeNative` / `FS_SAFE_NATIVE_MODE`. ([#815](https://github.com/openclaw/fs-safe/pull/815))
|
|
10
|
+
- **Watch overflow:** fold busy hints so unselected sibling churn beside a missing tree no longer overflows the watch. ([#813](https://github.com/openclaw/fs-safe/pull/813))
|
|
11
|
+
- **Windows exclusive creation:** no longer follows unchanged dangling file symlinks, including standalone creators, unrooted locks and cross-volume copy publication. ([#823](https://github.com/openclaw/fs-safe/pull/823), [#824](https://github.com/openclaw/fs-safe/pull/824))
|
|
12
|
+
- **Faster metadata listings:** reduce path normalization and per-entry allocation when listing Root directory metadata. ([#818](https://github.com/openclaw/fs-safe/pull/818), [#819](https://github.com/openclaw/fs-safe/pull/819))
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
- **Windows directory preparation:** retain exact file identities in `ensureAbsoluteDirectory()` so a replaced directory cannot compare equal after large Windows file IDs are rounded to JavaScript numbers. ([#825](https://github.com/openclaw/fs-safe/pull/825))
|
|
17
|
+
- **Windows writable creation:** in native `auto`/`off` mode, `openWritable()`, `append()`, and create-only writes could create a dangling file symlink's missing in-Root target before rejecting the link. Check final leaves before fallback creation; outside-Root targets were already rejected. ([#823](https://github.com/openclaw/fs-safe/pull/823))
|
|
18
|
+
- **Windows symlink creation escape:** unchanged dangling file symlinks could redirect standalone exclusive creation outside the caller-selected directory, including source contents during cross-volume copy publication and records from unrooted fallback locks. Check final leaves before creation, including internal staging files; concurrent replacement remains best-effort. ([#824](https://github.com/openclaw/fs-safe/pull/824))
|
|
19
|
+
- **Atomic writes:** preserve both parent-directory preparation and descriptor-close failures, in operation-first order, for synchronous and asynchronous replacements. ([#820](https://github.com/openclaw/fs-safe/pull/820))
|
|
20
|
+
- **Watch overflow:** fold busy pending hints into guarded subtree reconciliation so unselected sibling churn beside a missing tree no longer exhausts the hint budget. ([#813](https://github.com/openclaw/fs-safe/pull/813))
|
|
21
|
+
- **Native initialization:** lazily load the addon in fs-safe's import context so its housekeeping does not retain the first operation's `AsyncLocalStorage` stores when fs-safe is imported outside caller scopes. ([#797](https://github.com/openclaw/fs-safe/pull/797))
|
|
22
|
+
|
|
23
|
+
### Performance
|
|
24
|
+
|
|
25
|
+
- **Root metadata path assembly:** avoid renormalizing each literal child beneath its canonical directory, cutting 1,000-entry listing time by a further 19–23% and sampled allocations by 33–34% on Linux x64. ([#819](https://github.com/openclaw/fs-safe/pull/819))
|
|
26
|
+
- **Root metadata listings:** construct named metadata once per entry, reducing 1,000-entry listing time by 16% and sampled allocations by 27–28% on Linux x64, with all filesystem and identity checks retained. ([#818](https://github.com/openclaw/fs-safe/pull/818))
|
|
27
|
+
|
|
28
|
+
### Compatibility
|
|
29
|
+
|
|
30
|
+
- **Removed compatibility options:** remove `configureFsSafePython`, `FsSafePythonConfig`, `pythonPath`, and the six Python environment variables; use `configureFsSafeNative`, `FsSafeNativeConfig`, and `FS_SAFE_NATIVE_MODE` (or `OPENCLAW_FS_SAFE_NATIVE_MODE`). The Python variables are now ignored, so a deployment that still sets `FS_SAFE_PYTHON_MODE=require` or `off` runs in `auto` until it sets `FS_SAFE_NATIVE_MODE`. Remove secure-temp `chmodSync`, atomic adapter `chmod`/`chmodSync`, Root `nonBlockingRead`, and ACL `principalSids`; use descriptor-bound permission adapters (`descriptor` for secure-temp, `FileHandle.chmod` / `fchmodSync` for atomic writes), automatic nonblocking read admission, and direct SID classification. Callers passing removed options get TypeScript errors. ([#815](https://github.com/openclaw/fs-safe/pull/815))
|
|
31
|
+
|
|
32
|
+
## 0.22.0 - 2026-10-01
|
|
33
|
+
|
|
34
|
+
### Highlights
|
|
35
|
+
|
|
36
|
+
- **One-way publication:** `retainEntryForPublication` on `advanced` provides no-replace export for POSIX directories, regular files and direct symlinks, plus Windows NTFS files, directories, symbolic links and junctions. ([#776](https://github.com/openclaw/fs-safe/pull/776), [#778](https://github.com/openclaw/fs-safe/pull/778), [#779](https://github.com/openclaw/fs-safe/pull/779))
|
|
37
|
+
- **Windows path hardening:** reject foreign UNC shares and device namespaces before filesystem probes, blocking attacker-chosen SMB lookups, NTLM exposure and stalls. ([#781](https://github.com/openclaw/fs-safe/pull/781), [#784](https://github.com/openclaw/fs-safe/pull/784))
|
|
38
|
+
- **Watch overflow fixes:** Linux watch-retirement echoes no longer overflow unrelated subscriptions; Linux and macOS classify undecodable names, Linux preserves nameless lifecycle events, and macOS cleans up failed startup correctly. ([#782](https://github.com/openclaw/fs-safe/pull/782), [#783](https://github.com/openclaw/fs-safe/pull/783), [#769](https://github.com/openclaw/fs-safe/pull/769))
|
|
39
|
+
|
|
40
|
+
### Features
|
|
41
|
+
|
|
42
|
+
- **One-way publication:** `retainEntryForPublication` retains source and parent identities for native no-replace export on supported local POSIX filesystems and Windows NTFS. It supports directories, regular files and direct symbolic links, plus NTFS junctions; link targets stay opaque, and POSIX relative and dangling targets retain their exact bytes. Commit, indeterminate and close failures remain distinct. Requires caller-exclusive source namespaces, with no source-CAS or automatic rollback guarantee; disposal only closes retained resources and never deletes either location. ([#776](https://github.com/openclaw/fs-safe/pull/776), [#778](https://github.com/openclaw/fs-safe/pull/778), [#779](https://github.com/openclaw/fs-safe/pull/779))
|
|
43
|
+
|
|
44
|
+
### Fixes
|
|
45
|
+
|
|
46
|
+
- **Watch name handling:** ignore undecodable unselected siblings, fail closed for selected tree names on Linux and macOS, and preserve Linux directory lifecycle detail without spurious overflow. ([#783](https://github.com/openclaw/fs-safe/pull/783))
|
|
47
|
+
- **Windows UNC admission:** Root, root-file readers, `pathScope`, `isPathInside`, secret-file writers, sibling-temp output, trash admission, and archive output preparation reject foreign UNC shares and device namespaces before probing them, comparing hosts without Unicode case folding, preventing attacker-chosen SMB lookups, NTLM exposure, and stalls. Existing containment errors are preserved; absolute paths on another share no longer resolve through aliases back into the boundary. ([#781](https://github.com/openclaw/fs-safe/pull/781), [#784](https://github.com/openclaw/fs-safe/pull/784))
|
|
48
|
+
- **Linux watch isolation:** closing or replacing a watch, including after a scan failure, no longer overflows unrelated subscriptions. ([#782](https://github.com/openclaw/fs-safe/pull/782))
|
|
49
|
+
- **macOS watch startup:** clean up rejected FSEvents configuration and failed stream startup with the correct lifecycle calls, keeping the registration available for retry. ([#769](https://github.com/openclaw/fs-safe/pull/769))
|
|
50
|
+
|
|
51
|
+
### Performance
|
|
52
|
+
|
|
53
|
+
- **Bounded temporary-directory cleanup:** avoid a redundant descriptor metadata read for each descendant directory on Linux and macOS, while retaining fresh name identity checks before removal. ([#760](https://github.com/openclaw/fs-safe/pull/760))
|
|
54
|
+
- **Atomic temp cleanup:** use the file identity captured after creation, avoiding an unnecessary metadata lookup and preserving preexisting temporary paths. ([#772](https://github.com/openclaw/fs-safe/pull/772))
|
|
55
|
+
|
|
56
|
+
### Compatibility
|
|
57
|
+
|
|
58
|
+
- Windows now rejects foreign UNC shares and device namespaces before probing them. Absolute paths on another share no longer resolve through aliases back into the boundary; callers that relied on that behavior must pass in-boundary paths. ([#781](https://github.com/openclaw/fs-safe/pull/781), [#784](https://github.com/openclaw/fs-safe/pull/784))
|
|
59
|
+
|
|
60
|
+
## 0.21.3 - 2026-09-30
|
|
61
|
+
|
|
62
|
+
### Highlights
|
|
63
|
+
|
|
64
|
+
- **Watch transient files:** excluded files and atomic-save temp files no longer cause spurious overflow when they disappear between guarded passes. ([#773](https://github.com/openclaw/fs-safe/pull/773))
|
|
65
|
+
- **Faster watch rescans:** unrelated native hints skip rescans, and relevant hints reconcile affected directories, reducing scan work and allocation. ([#774](https://github.com/openclaw/fs-safe/pull/774))
|
|
66
|
+
|
|
67
|
+
### Fixes
|
|
68
|
+
|
|
69
|
+
- **Watch:** no longer reports overflow when an excluded or atomic-save temp file appears and disappears between guarded passes under the same observed parent directory. ([#773](https://github.com/openclaw/fs-safe/pull/773))
|
|
70
|
+
- **Windows removal:** nonrecursive `Root.remove()` reports `not-empty` for a nonempty directory in required native mode, including with `force: true`, instead of `not-removable`. ([#771](https://github.com/openclaw/fs-safe/pull/771))
|
|
71
|
+
- **Copy and staging failures:** preserve publication callback errors with unreadable metadata, retain uncertain native rename outcomes, and prevent repeated or reentrant cleanup from reusing consumed descriptors. ([#763](https://github.com/openclaw/fs-safe/pull/763))
|
|
72
|
+
|
|
73
|
+
### Performance
|
|
74
|
+
|
|
75
|
+
- **Watch:** skip guarded unrelated native hints and reconcile affected directories, reducing a 502-file memory-watch fixture from 513 to 55 visits and 8.16 MB to 1.33 MB sampled allocation per relevant event pass; unrelated detailed hints at two edits/second no longer trigger 120 full scans/minute, while periodic and uncertain-event reconciliation remain full. ([#774](https://github.com/openclaw/fs-safe/pull/774))
|
|
76
|
+
- **Linux fallback resolution:** reuse the file type from each component's checked descriptor observation, avoiding a duplicate metadata read while retaining fresh identity, symlink-target, and mount-policy checks. ([#757](https://github.com/openclaw/fs-safe/pull/757))
|
|
77
|
+
- **Native parent validation:** reuse the retained descriptor's captured identity during ordinary POSIX policy checks, avoiding a duplicate metadata read while keeping the final pathname inspection fresh. ([#735](https://github.com/openclaw/fs-safe/pull/735))
|
|
78
|
+
|
|
79
|
+
## 0.21.2 - 2026-09-28
|
|
80
|
+
|
|
81
|
+
### Highlights
|
|
82
|
+
|
|
83
|
+
- **`require` mode confines mutations under hostile concurrency:** with `FS_SAFE_NATIVE_MODE=require` (or `configureFsSafeNative({ mode: "require" })`), remove, recursive remove, `mkdir`, writable-open creation and overwrite `move` now run through retained, identity-checked native parents: descriptor-relative `unlinkat`, `mkdirat` and `renameat2` on POSIX, and handle-relative operations on Windows. An adversarial race fuzzer observed no outside or denied-path effects across about 2.8 million racing calls on Linux (with and without `openat2`), macOS and Windows. `auto` keeps its documented best-effort paths and performance. See the [operation/platform matrix](docs/security-model.md#native-root-mutation-capabilities). ([#744](https://github.com/openclaw/fs-safe/pull/744), [#751](https://github.com/openclaw/fs-safe/pull/751), [#750](https://github.com/openclaw/fs-safe/pull/750), [#752](https://github.com/openclaw/fs-safe/pull/752))
|
|
84
|
+
- **Watch on macOS:** entry scopes use identity-checked kqueue descriptors instead of recursive FSEvents streams, ending overflow storms for shallow ancestor scopes above busy trees. ([#753](https://github.com/openclaw/fs-safe/pull/753))
|
|
85
|
+
- **Faster watch scans:** watch readiness and reconciliation are 27–34% faster on large trees. ([#747](https://github.com/openclaw/fs-safe/pull/747))
|
|
86
|
+
|
|
87
|
+
### Fixes
|
|
88
|
+
|
|
89
|
+
- **Watch cancellation:** a subscription's `signal` closes it even when another abort listener stops event propagation. ([#738](https://github.com/openclaw/fs-safe/pull/738))
|
|
90
|
+
- **Walk cancellation:** walks stop without yielding the current entry when a synchronous filter aborts the signal. ([#739](https://github.com/openclaw/fs-safe/pull/739))
|
|
91
|
+
- **Writes with `mkdir: false`:** the JavaScript fallback writes through existing in-root parent aliases using the same guarded walk, instead of rejecting them. ([#740](https://github.com/openclaw/fs-safe/pull/740))
|
|
92
|
+
- **Descriptor exhaustion:** native writes that fail with `EMFILE`/`ENFILE` report `helper-failed` with the exhaustion code, the indeterminate publication outcome and the preserved stage, instead of a misleading path error. ([#742](https://github.com/openclaw/fs-safe/pull/742))
|
|
93
|
+
- **Walk memory:** `walkDirectory()` and `walkDirectorySync()` stop reading a directory once `maxEntries` is reached instead of enumerating it completely first. `Root.walk()` sorted mode stays deterministic. ([#743](https://github.com/openclaw/fs-safe/pull/743))
|
|
94
|
+
|
|
95
|
+
### Compatibility
|
|
96
|
+
|
|
97
|
+
- In `require` mode, the operations above now reject with `helper-unavailable` where no confining native primitive exists, instead of silently falling back to JavaScript. Examples are recursive removal on Linux without `openat2` and recursive directory removal on Windows. Use `auto` for the best-effort fallback. These `require`-mode mutations also do more identity-checked work and are slower than before, while `auto` is unchanged. ([#744](https://github.com/openclaw/fs-safe/pull/744), [#751](https://github.com/openclaw/fs-safe/pull/751), [#750](https://github.com/openclaw/fs-safe/pull/750))
|
|
98
|
+
- Create-only writes to an existing directory report `already-exists` in every implementation (previously `not-file` on some Windows paths). With `mutationSymlinks` omitted, Windows native and legacy JavaScript writes still differ for final links and parent junctions; set `mutationSymlinks: "reject"` for uniform behavior. ([#745](https://github.com/openclaw/fs-safe/pull/745))
|
|
99
|
+
|
|
5
100
|
## 0.21.1 - 2026-09-27
|
|
6
101
|
|
|
7
102
|
### Fixes
|