chore(deps): update fs-safe to 0.23.0 (#163408)

* chore(deps): update fs-safe to 0.23.0

* fix(fs-safe): complete migration tooling metadata
This commit is contained in:
Peter Steinberger 2026-10-02 07:14:28 -05:00 • committed by GitHub
parent ef27c1fc2b
commit bb40f2c70b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 913 additions and 200 deletions

View file

@ -1974,7 +1974,7 @@ src/config/channel-alias-migration.ts 2
src/config/channel-capabilities.ts 2
src/config/channel-config-metadata.ts 1
src/config/channel-doctor-helpers.ts 5
src/config/config-env-vars.ts 2
src/config/config-env-vars.ts 1
src/config/config-journal-snapshot.ts 2
src/config/context-visibility.ts 1
src/config/defaults.ts 6

View file

@ -4,4 +4,5 @@
# The internal Node-version check no longer contributes a false OPENCLAW_* name.
# The next-turn runtime-context preface constant no longer exists as an OPENCLAW_* name.
# OPENCLAW_PACKAGE_BUN_LAUNCHER (#159431) is the updater-to-preinstall handoff for Bun-only installs.
471
# fs-safe 0.23 moves five existing native/Python environment names into OpenClaw's compatibility owner.
476

View file

@ -40,7 +40,9 @@ The generic fs-safe environment name also works: `FS_SAFE_NATIVE_MODE`.
[Managed worktree acceleration](/concepts/managed-worktrees#filesystem-acceleration) uses isolated native operations for APFS and Btrfs cloning and metadata reads. Those operations retain automatic native selection without changing the Gateway process's configuration. An explicit native mode applies to the isolated operations too; `off` selects normal Git checkout. Native writes remain owned by a supervised child until it exits, so cancellation cannot release the destination for cleanup while the child is still writing.
fs-safe still maps the retired `FS_SAFE_PYTHON_MODE` and `OPENCLAW_FS_SAFE_PYTHON_MODE` values to native modes with a deprecation warning. Replace them with `FS_SAFE_NATIVE_MODE` or `OPENCLAW_FS_SAFE_NATIVE_MODE`. Python interpreter path settings are no longer used.
fs-safe 0.23 removes the Python bridge. OpenClaw keeps `FS_SAFE_PYTHON_MODE` and `OPENCLAW_FS_SAFE_PYTHON_MODE` as deprecated mode aliases when loading its runtime environment, with a deprecation warning. Explicit native settings and programmatic `configureFsSafeNative()` still take precedence. Replace the old names with `FS_SAFE_NATIVE_MODE` or `OPENCLAW_FS_SAFE_NATIVE_MODE`.
Python interpreter paths are not used. Remove `FS_SAFE_PYTHON`, `OPENCLAW_FS_SAFE_PYTHON`, `OPENCLAW_PINNED_PYTHON`, and `OPENCLAW_PINNED_WRITE_PYTHON` from deployments. Code that directly uses fs-safe must replace `configureFsSafePython` / `FsSafePythonConfig` with `configureFsSafeNative` / `FsSafeNativeConfig` and omit `pythonPath`.
Use `require` when all native-capable operations must fail if the platform binding is unavailable. `auto` allows documented JavaScript fallbacks; no-clobber Root moves and Windows secure credential reads always require their native primitives.
@ -114,6 +116,8 @@ In `require` mode, an unavailable or unloadable helper normally causes `helper-u
- Plugin-facing file access should use `openclaw/plugin-sdk/*` helpers when a path comes from a message, model output, config, or plugin input. Plugins can use reviewed fs-safe primitives directly when they declare their own fs-safe dependency and retain the applicable path policy.
- Core code should import fs-safe primitives from their focused package entry points. Keep OpenClaw adapters where they own behavior, including secret-directory mode repair, archive durability, producer isolation, and public SDK compatibility. Pure re-exports are unnecessary: fs-safe owns its process defaults.
- OpenClaw's Plugin SDK retains the deprecated `nonBlockingRead` input hint for existing callers; omit it in new code. Safe reads always use nonblocking admission where supported, including when the old hint is `false`. Direct fs-safe calls no longer accept this option.
- The SDK's atomic replacement helper also retains the ignored adapter `chmod` member for source compatibility. Direct fs-safe adapters must omit it; permissions use the retained file handle.
- Archive extraction should use the fs-safe archive helpers with explicit size, entry-count, link, and destination limits.
- Secrets should use OpenClaw secret helpers or fs-safe secret/private-state helpers. Do not hand-roll mode checks around `fs.writeFile`.
- For hostile local-user isolation, do not rely on fs-safe alone. Run separate gateways under separate OS users/hosts, or use sandboxing.

View file

@ -10,7 +10,7 @@
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.79.0",
"@agentclientprotocol/codex-acp": "1.12.0",
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"acpx": "0.19.1",
"smol-toml": "1.8.0",
"zod": "4.6.5"

View file

@ -9,7 +9,7 @@
"type": "module",
"dependencies": {
"@openai/codex": "0.159.1",
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"semver": "7.8.5",
"smol-toml": "1.8.0",
"typebox": "1.3.34",

View file

@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"@trycua/cua-driver": "0.28.2",
"rastermill": "0.3.3",
"zod": "4.6.5"

View file

@ -9,7 +9,7 @@
"type": "module",
"dependencies": {
"@larksuiteoapi/node-sdk": "1.74.0",
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"mdast-util-from-markdown": "2.0.3",
"mdast-util-gfm-table": "2.0.0",
"micromark-extension-gfm-table": "2.1.2",

View file

@ -4,7 +4,7 @@
"description": "OpenClaw file transfer plugin (file_fetch, dir_list, dir_fetch, file_write)",
"type": "module",
"dependencies": {
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"minimatch": "10.2.6",
"typebox": "1.3.34"
},

View file

@ -33,6 +33,6 @@
}
},
"dependencies": {
"@openclaw/fs-safe": "0.22.0"
"@openclaw/fs-safe": "0.23.0"
}
}

View file

@ -10,7 +10,7 @@
"dependencies": {
"@matrix-org/matrix-sdk-crypto-nodejs": "0.6.6",
"@matrix-org/matrix-sdk-crypto-wasm": "18.8.0",
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"fake-indexeddb": "6.2.5",
"markdown-it": "15.0.2",
"matrix-js-sdk": "42.4.0",

View file

@ -10,7 +10,7 @@
"./worker-api.js": "./worker-api.ts"
},
"dependencies": {
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"json5": "2.2.3",
"p-limit": "7.3.3",
"typebox": "1.3.34"

View file

@ -200,7 +200,6 @@ export async function commitMemoryContent(
fileSystem: {
promises: {
mkdir: fs.mkdir,
chmod: fs.chmod,
writeFile: fs.writeFile,
rename: async (from, to) => {
publication.state = "uncertain";

View file

@ -22,6 +22,6 @@
]
},
"dependencies": {
"@openclaw/fs-safe": "0.22.0"
"@openclaw/fs-safe": "0.23.0"
}
}

View file

@ -8,7 +8,7 @@
},
"type": "module",
"dependencies": {
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"p-limit": "7.3.3",
"zod": "4.6.5"
},

View file

@ -8,7 +8,7 @@
"@copilotkit/aimock": "1.43.0",
"@modelcontextprotocol/sdk": "1.30.1",
"@openclaw/crabline": "0.1.28",
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"json5": "2.2.3",
"playwright-core": "1.63.0",
"semver": "7.8.5",

View file

@ -4,7 +4,7 @@
"description": "OpenClaw Signal channel plugin",
"type": "module",
"dependencies": {
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"ws": "8.21.3",
"zod": "4.6.5"
},

View file

@ -2255,7 +2255,7 @@
"@modelcontextprotocol/sdk": "1.30.1",
"@mozilla/readability": "0.6.0",
"@openclaw/ai": "workspace:*",
"@openclaw/fs-safe": "0.22.0",
"@openclaw/fs-safe": "0.23.0",
"@openclaw/proxyline": "0.3.12",
"@silvia-odwyer/photon-node": "0.3.4",
"@trycua/cua-driver": "0.28.2",

114
pnpm-lock.yaml generated
View file

@ -276,8 +276,8 @@ importers:
specifier: workspace:*
version: link:packages/ai
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
'@openclaw/proxyline':
specifier: 0.3.12
version: 0.3.12(patch_hash=9e0969d2dc0abb32610053fba342756afb56f4996c90adfdc34f21ad090fbb0f)(undici@8.11.2)
@ -645,8 +645,8 @@ importers:
specifier: 1.12.0
version: 1.12.0
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
acpx:
specifier: 0.19.1
version: 0.19.1
@ -937,8 +937,8 @@ importers:
specifier: 0.159.1
version: 0.159.1
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
semver:
specifier: 7.8.5
version: 7.8.5
@ -996,8 +996,8 @@ importers:
extensions/cua-computer:
dependencies:
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
'@trycua/cua-driver':
specifier: 0.28.2
version: 0.28.2
@ -1209,8 +1209,8 @@ importers:
specifier: 1.74.0
version: 1.74.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2)
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
mdast-util-from-markdown:
specifier: 2.0.3
version: 2.0.3(supports-color@10.2.2)
@ -1237,8 +1237,8 @@ importers:
extensions/file-transfer:
dependencies:
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
minimatch:
specifier: 10.2.6
version: 10.2.6
@ -1479,8 +1479,8 @@ importers:
extensions/llama-cpp:
dependencies:
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
@ -1535,8 +1535,8 @@ importers:
specifier: 18.8.0
version: 18.8.0
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
fake-indexeddb:
specifier: 6.2.5
version: 6.2.5
@ -1579,8 +1579,8 @@ importers:
extensions/memory-core:
dependencies:
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
json5:
specifier: 2.2.3
version: 2.2.3
@ -1689,8 +1689,8 @@ importers:
extensions/migrate-claude:
dependencies:
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
devDependencies:
'@openclaw/plugin-sdk':
specifier: workspace:*
@ -1919,8 +1919,8 @@ importers:
extensions/openshell:
dependencies:
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
p-limit:
specifier: 7.3.3
version: 7.3.3
@ -1994,8 +1994,8 @@ importers:
specifier: 0.1.28
version: 0.1.28
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
json5:
specifier: 2.2.3
version: 2.2.3
@ -2126,8 +2126,8 @@ importers:
extensions/signal:
dependencies:
'@openclaw/fs-safe':
specifier: 0.22.0
version: 0.22.0
specifier: 0.23.0
version: 0.23.0
ws:
specifier: 8.21.3
version: 8.21.3
@ -4630,8 +4630,8 @@ packages:
cpu: [arm64]
os: [darwin]
'@openclaw/fs-safe-darwin-arm64@0.22.0':
resolution: {integrity: sha512-u7HSbjJAIiWZ0iFJzoXymMJUn0EMzJGMFtX7aIbQk2W6Sz+UxTuXszJk79yYNF46kxKXjlu9Vkx3oazT/6mE5g==}
'@openclaw/fs-safe-darwin-arm64@0.23.0':
resolution: {integrity: sha512-Apna2HUA6q2r6OBs+MqU6dEWCECeIgLVdpvncS+lvTIvOlOGbDlIzQO+IWlmKhNMJhwljGt493fgI66pylOxxA==}
engines: {node: '>=22'}
cpu: [arm64]
os: [darwin]
@ -4642,8 +4642,8 @@ packages:
cpu: [x64]
os: [darwin]
'@openclaw/fs-safe-darwin-x64@0.22.0':
resolution: {integrity: sha512-rkcMsgbQQzNFcEjq74J0hbwMoHlVXtQzC5owdAU6QG1O2UTGIPROpwNXmo1Phmz4tSTXwCKlH5vxxSgSHMv8Mw==}
'@openclaw/fs-safe-darwin-x64@0.23.0':
resolution: {integrity: sha512-vpH76FbxexkwDYxIGXd7nQlhehUPOrMP6bdXuOKnXaHqBIL3MpBrf8GT7VAqTTjGWsEXHbGr135Jr3H8Ir4RdA==}
engines: {node: '>=22'}
cpu: [x64]
os: [darwin]
@ -4655,8 +4655,8 @@ packages:
os: [linux]
libc: [glibc]
'@openclaw/fs-safe-linux-arm64-gnu@0.22.0':
resolution: {integrity: sha512-nwZl03/waPnBh7lKFnDiF7RfT85D9Ngbw23LL7jb2ilXu32zge4gqufYYIoLXk0EGL3bLkwssgnpxy7ErRQWow==}
'@openclaw/fs-safe-linux-arm64-gnu@0.23.0':
resolution: {integrity: sha512-2Lh8UiQSE4F3AgMTMeC8WaXoyQeH3kErtI/7bv/N6JsJVI52LyKXlIXVAoOhWT/ZFUmc2m4qxAzJ/94vWv2VhQ==}
engines: {node: '>=22'}
cpu: [arm64]
os: [linux]
@ -4669,8 +4669,8 @@ packages:
os: [linux]
libc: [musl]
'@openclaw/fs-safe-linux-arm64-musl@0.22.0':
resolution: {integrity: sha512-2Gvr1vUvSpbdkAeUuYU8DmgzRGPx88V4quX1AWNZdFuip9SsGkbaBLx3kZlR4OFlEo6HxgGPS7KZanaG17nKSg==}
'@openclaw/fs-safe-linux-arm64-musl@0.23.0':
resolution: {integrity: sha512-V1Ql6caV7WRXpyn3cbqtxTMXR1J1fP56DSkA/dAFPn0SejixRH3yo3mM9h+iL3Ng7QR7cFy8LkIK/JXbi0Tbbg==}
engines: {node: '>=22'}
cpu: [arm64]
os: [linux]
@ -4683,8 +4683,8 @@ packages:
os: [linux]
libc: [glibc]
'@openclaw/fs-safe-linux-x64-gnu@0.22.0':
resolution: {integrity: sha512-xTb3icn5X+SREL30NNXy1ZgjkGGWanRm9Wsk5pGm96Iy1DWY18HRyjnhDHCthZ402JiVBXCJR/8nsdHI0loaRg==}
'@openclaw/fs-safe-linux-x64-gnu@0.23.0':
resolution: {integrity: sha512-1jAzULyvK1+30Bg780Uyx/wZQ7GX1AuRxtniUh5QRjw568TTo/S1MC5tVlZqdZPAz2EPFHQRYb/fquPh1kIUeg==}
engines: {node: '>=22'}
cpu: [x64]
os: [linux]
@ -4697,8 +4697,8 @@ packages:
os: [linux]
libc: [musl]
'@openclaw/fs-safe-linux-x64-musl@0.22.0':
resolution: {integrity: sha512-oOxSQNtPxEJRDUsydXX4TVw/vGXKxUR1bW8wieEsxlhJob+fbRcSZb6YyznPkGiirhcpCwwXgQwQbaD+9Ho9Hg==}
'@openclaw/fs-safe-linux-x64-musl@0.23.0':
resolution: {integrity: sha512-NIQarT20KCPJkzbjbte3bC5JT6yNi8TqfHWFGvdKu1piU+lvQbzVeUlQupvAGsrlyCAumRjlUdZfi0MPOxNxmQ==}
engines: {node: '>=22'}
cpu: [x64]
os: [linux]
@ -4710,8 +4710,8 @@ packages:
cpu: [x64]
os: [win32]
'@openclaw/fs-safe-win32-x64-msvc@0.22.0':
resolution: {integrity: sha512-V0MR0A3nYbvJofxdcKfgp8sU46UDAESV2wxh4w4+coxPkfEFfedJa2tz0L1Fc1wFEcZPXIbywXNhOy2qF6QZug==}
'@openclaw/fs-safe-win32-x64-msvc@0.23.0':
resolution: {integrity: sha512-eT8FvQu2Lr7vV4hhakGVHAXOICV1+Hgno1QgJuJ5XvEZa4aRy67HNKKlfWZK2m1X+lfWQIgeDKxEtSG1ajsRxQ==}
engines: {node: '>=22'}
cpu: [x64]
os: [win32]
@ -4720,8 +4720,8 @@ packages:
resolution: {integrity: sha512-Dwu2BZL6+n9pTeSLwijjL+dUBN0ktOjyucZPDjwSpuIkiPx9aussFWdyiWUD9UvNu3gwfLRAcktz1HfYT8QW1w==}
engines: {node: '>=22'}
'@openclaw/fs-safe@0.22.0':
resolution: {integrity: sha512-CW/T0NUcdn8PLVBgyXUbIn90a16uhP3YtKdd+eqJjmg0FR01KCCjCI3PxrvK1ygrUb/BfQsJJe+hXA9+/0rqKQ==}
'@openclaw/fs-safe@0.23.0':
resolution: {integrity: sha512-4AQjaT2KF3Kmt+DYabyJpWIJcm0CXaEIvDRbR4g4PmkG5dc8KtNf5kahMhQ7F/n1DzMztmSLWilt07m01fDI1Q==}
engines: {node: '>=22'}
'@openclaw/libterminal@0.3.7':
@ -12327,43 +12327,43 @@ snapshots:
'@openclaw/fs-safe-darwin-arm64@0.12.0':
optional: true
'@openclaw/fs-safe-darwin-arm64@0.22.0':
'@openclaw/fs-safe-darwin-arm64@0.23.0':
optional: true
'@openclaw/fs-safe-darwin-x64@0.12.0':
optional: true
'@openclaw/fs-safe-darwin-x64@0.22.0':
'@openclaw/fs-safe-darwin-x64@0.23.0':
optional: true
'@openclaw/fs-safe-linux-arm64-gnu@0.12.0':
optional: true
'@openclaw/fs-safe-linux-arm64-gnu@0.22.0':
'@openclaw/fs-safe-linux-arm64-gnu@0.23.0':
optional: true
'@openclaw/fs-safe-linux-arm64-musl@0.12.0':
optional: true
'@openclaw/fs-safe-linux-arm64-musl@0.22.0':
'@openclaw/fs-safe-linux-arm64-musl@0.23.0':
optional: true
'@openclaw/fs-safe-linux-x64-gnu@0.12.0':
optional: true
'@openclaw/fs-safe-linux-x64-gnu@0.22.0':
'@openclaw/fs-safe-linux-x64-gnu@0.23.0':
optional: true
'@openclaw/fs-safe-linux-x64-musl@0.12.0':
optional: true
'@openclaw/fs-safe-linux-x64-musl@0.22.0':
'@openclaw/fs-safe-linux-x64-musl@0.23.0':
optional: true
'@openclaw/fs-safe-win32-x64-msvc@0.12.0':
optional: true
'@openclaw/fs-safe-win32-x64-msvc@0.22.0':
'@openclaw/fs-safe-win32-x64-msvc@0.23.0':
optional: true
'@openclaw/fs-safe@0.12.0':
@ -12377,15 +12377,15 @@ snapshots:
'@openclaw/fs-safe-win32-x64-msvc': 0.12.0
jszip: 3.10.2
'@openclaw/fs-safe@0.22.0':
'@openclaw/fs-safe@0.23.0':
optionalDependencies:
'@openclaw/fs-safe-darwin-arm64': 0.22.0
'@openclaw/fs-safe-darwin-x64': 0.22.0
'@openclaw/fs-safe-linux-arm64-gnu': 0.22.0
'@openclaw/fs-safe-linux-arm64-musl': 0.22.0
'@openclaw/fs-safe-linux-x64-gnu': 0.22.0
'@openclaw/fs-safe-linux-x64-musl': 0.22.0
'@openclaw/fs-safe-win32-x64-msvc': 0.22.0
'@openclaw/fs-safe-darwin-arm64': 0.23.0
'@openclaw/fs-safe-darwin-x64': 0.23.0
'@openclaw/fs-safe-linux-arm64-gnu': 0.23.0
'@openclaw/fs-safe-linux-arm64-musl': 0.23.0
'@openclaw/fs-safe-linux-x64-gnu': 0.23.0
'@openclaw/fs-safe-linux-x64-musl': 0.23.0
'@openclaw/fs-safe-win32-x64-msvc': 0.23.0
jszip: 3.10.2
'@openclaw/libterminal@0.3.7':

View file

@ -12,15 +12,15 @@ minimumReleaseAgeStrict: true
minimumReleaseAgeExclude:
- "@openai/codex"
- "@openai/codex-*"
# Reviewed fs-safe 0.22.0 release and native helpers; remove after 2026-10-08T06:00:12.449Z.
- "@openclaw/fs-safe@0.22.0"
- "@openclaw/fs-safe-darwin-arm64@0.22.0"
- "@openclaw/fs-safe-darwin-x64@0.22.0"
- "@openclaw/fs-safe-linux-arm64-gnu@0.22.0"
- "@openclaw/fs-safe-linux-arm64-musl@0.22.0"
- "@openclaw/fs-safe-linux-x64-gnu@0.22.0"
- "@openclaw/fs-safe-linux-x64-musl@0.22.0"
- "@openclaw/fs-safe-win32-x64-msvc@0.22.0"
# Reviewed fs-safe 0.23.0 release and native helpers; remove after 2026-10-09T06:15:08.733Z.
- "@openclaw/fs-safe@0.23.0"
- "@openclaw/fs-safe-darwin-arm64@0.23.0"
- "@openclaw/fs-safe-darwin-x64@0.23.0"
- "@openclaw/fs-safe-linux-arm64-gnu@0.23.0"
- "@openclaw/fs-safe-linux-arm64-musl@0.23.0"
- "@openclaw/fs-safe-linux-x64-gnu@0.23.0"
- "@openclaw/fs-safe-linux-x64-musl@0.23.0"
- "@openclaw/fs-safe-win32-x64-msvc@0.23.0"
# Isolated installs let pnpm reuse whole-package APFS clones instead of relinking every file.
nodeLinker: isolated

View file

@ -736,6 +736,7 @@ src/infra/format-time/duration-units.ts
src/infra/format-time/format-duration-exact.ts
src/infra/format-time/format-duration-internal.ts
src/infra/fs-safe-copy.worker.ts
src/infra/fs-safe-env.ts
src/infra/fs-safe.ts
src/infra/gateway-lock-process.ts
src/infra/gateway-owner-lease.read.ts

View file

@ -280,7 +280,6 @@ async function writeIdentityFile(params: {
try {
const result = await workspaceRoot.read(DEFAULT_IDENTITY_FILENAME, {
hardlinks: "reject",
nonBlockingRead: true,
});
existing = result.buffer.toString("utf-8");
} catch (error) {

View file

@ -260,7 +260,6 @@ export async function validateScriptFileForShellBleed(params: {
})
: (
await workspaceRoot.read(relativePath, {
nonBlockingRead: true,
symlinks: "follow-within-root",
maxBytes: SCRIPT_PREFLIGHT_MAX_BYTES,
})

View file

@ -84,25 +84,27 @@ describe.skipIf(process.platform !== "darwin")("isolated native worktree operati
expect(getFsSafeNativeConfig().mode).toBe("off");
});
it.each(["FS_SAFE_NATIVE_MODE", "OPENCLAW_FS_SAFE_NATIVE_MODE"])(
"honors explicit %s=off in read and write children",
async (name) => {
vi.stubEnv(name, "off");
const root = tempDirs.make("openclaw-native-disabled-");
const source = path.join(root, "source");
const destination = path.join(root, "destination");
await fs.mkdir(source);
await fs.writeFile(path.join(source, "payload"), "source");
expect(await detectWorktreeFilesystemBackend(root, options)).toBeNull();
await expect(
nativeWorktreeFilesystem.copy(source, destination, options),
).rejects.toMatchObject({
it.each([
"FS_SAFE_NATIVE_MODE",
"OPENCLAW_FS_SAFE_NATIVE_MODE",
"FS_SAFE_PYTHON_MODE",
"OPENCLAW_FS_SAFE_PYTHON_MODE",
])("honors explicit %s=off in read and write children", async (name) => {
vi.stubEnv(name, "off");
const root = tempDirs.make("openclaw-native-disabled-");
const source = path.join(root, "source");
const destination = path.join(root, "destination");
await fs.mkdir(source);
await fs.writeFile(path.join(source, "payload"), "source");
expect(await detectWorktreeFilesystemBackend(root, options)).toBeNull();
await expect(nativeWorktreeFilesystem.copy(source, destination, options)).rejects.toMatchObject(
{
code: "helper-unavailable",
});
await expect(fs.access(destination)).rejects.toMatchObject({ code: "ENOENT" });
expect(getFsSafeNativeConfig().mode).toBe("off");
},
);
},
);
await expect(fs.access(destination)).rejects.toMatchObject({ code: "ENOENT" });
expect(getFsSafeNativeConfig().mode).toBe("off");
});
it.each(["copy", "createSource"])(
"revalidates allocation authority before child %s input",

View file

@ -268,7 +268,6 @@ async function readAuthorBootstrap(path: string): Promise<Buffer> {
const read = await sourceRoot.read(basename(resolvedPath), {
hardlinks: "reject",
maxBytes: MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES,
nonBlockingRead: true,
symlinks: "reject",
});
const text = new TextDecoder("utf-8", { fatal: true }).decode(read.buffer);

View file

@ -39,7 +39,6 @@ export async function readSelectedWorkspaceFiles(
const read = await root.read(name, {
hardlinks: "reject",
maxBytes: MAX_MANAGED_FILE_BYTES,
nonBlockingRead: true,
symlinks: "reject",
});
let text: string;

View file

@ -173,7 +173,6 @@ export async function readClawOpenClawProfile(params: {
const read = await profileFiles.read(declaredPath, {
hardlinks: "reject",
maxBytes: MAX_PROFILE_BYTES,
nonBlockingRead: true,
symlinks: "reject",
});
raw = read.buffer;

View file

@ -34,7 +34,6 @@ async function readSelectedProjectFile(projectRoot: string, path: string): Promi
const read = await sourceRoot.read(path, {
hardlinks: "reject",
maxBytes: MAX_MANAGED_FILE_BYTES,
nonBlockingRead: true,
symlinks: path === "CLAW.md" ? "follow-within-root" : "reject",
});
return read.buffer;

View file

@ -294,7 +294,6 @@ export async function validateClawProject(
const read = await sourceRoot.read("package.json", {
hardlinks: "reject",
maxBytes: MAX_PACKAGE_JSON_BYTES,
nonBlockingRead: true,
symlinks: "reject",
});
packageValue = JSON.parse(read.buffer.toString("utf8"));

View file

@ -41,7 +41,6 @@ async function readBoundedFile(path: string, maxBytes: number): Promise<Buffer>
const read = await fileRoot.read(basename(path), {
hardlinks: "reject",
maxBytes,
nonBlockingRead: true,
symlinks: "reject",
});
return read.buffer;
@ -150,7 +149,6 @@ async function buildDevelopmentSnapshot(params: {
const read = await sourceRoot.read("BOOTSTRAP.md", {
hardlinks: "reject",
maxBytes: MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES,
nonBlockingRead: true,
symlinks: "reject",
});
const text = new TextDecoder("utf-8", { fatal: true }).decode(read.buffer);

View file

@ -9,6 +9,11 @@ import { GATEWAY_CONFIG_SELECTION_ENV_KEYS } from "../../config/gateway-env-sele
import { CONFIG_AUDIT_STORE_LABEL } from "../../config/io.audit.js";
import { describeConfigSnapshotInputChange } from "../../config/snapshot-inputs.js";
import type { ConfigFileSnapshot } from "../../config/types.js";
import {
clearFsSafeEnvFallback,
fsSafeEnvInput,
normalizeFsSafeNativeEnv,
} from "../../infra/fs-safe-env.js";
import { ExitError, type RuntimeEnv } from "../../runtime.js";
import { withArtifactPreservingStateReads } from "../../state/openclaw-state-db-readonly.js";
import { formatCliCommand } from "../command-format.js";
@ -130,6 +135,7 @@ function restoreGatewayEnvChanges(params: {
after: Record<string, string | undefined>;
preservedKeys?: ReadonlySet<string>;
}): void {
clearFsSafeEnvFallback(process.env);
const keys = new Set([...Object.keys(params.before), ...Object.keys(params.after)]);
for (const key of keys) {
const preservedKey = process.platform === "win32" ? key.toUpperCase() : key;
@ -146,6 +152,7 @@ function restoreGatewayEnvChanges(params: {
process.env[key] = previous;
}
}
normalizeFsSafeNativeEnv(process.env);
}
function restoreSupersededGatewaySelectionEnv(params: {
@ -154,7 +161,7 @@ function restoreSupersededGatewaySelectionEnv(params: {
}): void {
restoreGatewayEnvChanges({
before: params.beforeCurrentPass,
after: { ...process.env },
after: { ...fsSafeEnvInput(process.env) },
preservedKeys: GATEWAY_CONFIG_SELECTION_ENV_KEYS,
});
if (params.environmentSelection) {
@ -265,19 +272,19 @@ async function guardGatewayRunSelectedConfig(
const applySelectedConfigEnv = (snapshot: ConfigFileSnapshot) => {
restoreAppliedGatewayRunConfigEnvironment(params.opts.reset !== true);
if (snapshot.valid && params.opts.reset !== true) {
const envBeforeApply = { ...process.env };
const envBeforeApply = { ...fsSafeEnvInput(process.env) };
applyConfigEnvVars(snapshot.sourceConfig, process.env);
normalizeStateDirEnv(process.env);
normalizeEnv();
appliedGatewayRunConfigEnvironment = {
before: envBeforeApply,
after: { ...process.env },
after: { ...fsSafeEnvInput(process.env) },
};
}
applyInvocationDestructiveOverride(invocationDestructiveOverride);
};
for (;;) {
const envBeforeTrustedApply = { ...process.env };
const envBeforeTrustedApply = { ...fsSafeEnvInput(process.env) };
const trustedSelectionSignature = resolveGatewayConfigSelectionSignature(process.env);
const trustedEnvLoad = applyTrustedGatewayEnv(invocationDestructiveOverride);
if (resolveGatewayConfigSelectionSignature(process.env) !== trustedSelectionSignature) {
@ -292,7 +299,7 @@ async function guardGatewayRunSelectedConfig(
);
restoreGatewayEnvChanges({
before: envBeforeTrustedApply,
after: { ...process.env },
after: { ...fsSafeEnvInput(process.env) },
preservedKeys: new Set(
[...GATEWAY_CONFIG_SELECTION_ENV_KEYS].filter((key) => !fallbackSelectorKeys.has(key)),
),
@ -346,7 +353,7 @@ async function guardGatewayRunSelectedConfig(
async function guardGatewayRunReset(params: GatewayRunGuardParams): Promise<boolean> {
gatewayRunTargetSelectedByConfig = false;
const envBeforeGuard = { ...process.env };
const envBeforeGuard = { ...fsSafeEnvInput(process.env) };
try {
return await guardGatewayRunSelectedConfig(params);
} finally {
@ -357,7 +364,7 @@ async function guardGatewayRunReset(params: GatewayRunGuardParams): Promise<bool
// config being deleted must not survive into the replacement config or gateway runtime.
restoreGatewayEnvChanges({
before: envBeforeGuard,
after: { ...process.env },
after: { ...fsSafeEnvInput(process.env) },
preservedKeys: GATEWAY_RESET_SELECTION_ENV_KEYS,
});
}
@ -417,7 +424,7 @@ export async function applyFinalGatewayRunConfigEnv(params: {
return true;
}
const invocationDestructiveOverride = resolveInvocationDestructiveOverride();
const envBeforeApply = { ...process.env };
const envBeforeApply = { ...fsSafeEnvInput(process.env) };
const selectionSignature = resolveGatewayConfigSelectionSignature(process.env);
const [
{
@ -449,7 +456,7 @@ export async function applyFinalGatewayRunConfigEnv(params: {
return false;
}
restoreAppliedGatewayRunConfigEnvironment();
const envBeforeConfigApply = { ...process.env };
const envBeforeConfigApply = { ...fsSafeEnvInput(process.env) };
const replacedLowerPrecedenceKeys: string[] = [];
applyConfigEnvVars(params.snapshot.sourceConfig, process.env, {
lowerPrecedenceEnv: params.lowerPrecedenceEnv,
@ -463,7 +470,7 @@ export async function applyFinalGatewayRunConfigEnv(params: {
applyInvocationDestructiveOverride(invocationDestructiveOverride);
appliedGatewayRunConfigEnvironment = {
before: envBeforeApply,
after: { ...process.env },
after: { ...fsSafeEnvInput(process.env) },
};
if (resolveGatewayConfigSelectionSignature(process.env) === selectionSignature) {
initializePublishedConfigRuntimeEnv(params.snapshot.sourceConfig, {
@ -477,7 +484,7 @@ export async function applyFinalGatewayRunConfigEnv(params: {
return true;
}
appliedGatewayRunConfigEnvironment = undefined;
restoreGatewayEnvChanges({ before: envBeforeApply, after: { ...process.env } });
restoreGatewayEnvChanges({ before: envBeforeApply, after: { ...fsSafeEnvInput(process.env) } });
params.runtime.error(
"Refusing to start the gateway because the final config read changed config or state selection. Retry startup so the selected target can be validated.",
);
@ -532,14 +539,17 @@ export async function reloadTrustedGatewayRunEnvironment(params: {
runtime: RuntimeEnv;
}): Promise<boolean> {
const applyTrustedGatewayEnv = await createTrustedGatewayEnvLoader();
const envBeforeReload = { ...process.env };
const envBeforeReload = { ...fsSafeEnvInput(process.env) };
const selectionSignature = resolveGatewayConfigSelectionSignature(process.env);
const invocationDestructiveOverride = resolveInvocationDestructiveOverride();
applyTrustedGatewayEnv(invocationDestructiveOverride);
if (resolveGatewayConfigSelectionSignature(process.env) !== selectionSignature) {
// Runtime modules already derived process-stable paths before startup mutations. A replacement
// dotenv cannot select another target without splitting the running gateway across state dirs.
restoreGatewayEnvChanges({ before: envBeforeReload, after: { ...process.env } });
restoreGatewayEnvChanges({
before: envBeforeReload,
after: { ...fsSafeEnvInput(process.env) },
});
applyInvocationDestructiveOverride(invocationDestructiveOverride);
await pinGatewayRunRuntimePaths();
params.runtime.error(
@ -557,7 +567,7 @@ export async function selectGatewayRunEnvironment(params: GatewayRunGuardParams)
preparedGatewayRunBootstrap = undefined;
preparedGatewayRunReset = undefined;
restoreAppliedGatewayRunConfigEnvironment(params.opts.reset !== true);
const envBeforeGuard = { ...process.env };
const envBeforeGuard = { ...fsSafeEnvInput(process.env) };
selectedGatewayRunEnvironment = undefined;
let guarded: boolean;
try {
@ -567,14 +577,14 @@ export async function selectGatewayRunEnvironment(params: GatewayRunGuardParams)
restoreAppliedGatewayRunConfigEnvironment(false);
restoreGatewayEnvChanges({
before: envBeforeGuard,
after: { ...process.env },
after: { ...fsSafeEnvInput(process.env) },
preservedKeys: GATEWAY_RESET_SELECTION_ENV_KEYS,
});
}
}
selectedGatewayRunEnvironment = {
before: envBeforeGuard,
after: { ...process.env },
after: { ...fsSafeEnvInput(process.env) },
};
await pinGatewayRunRuntimePaths();
return guarded;

View file

@ -3,6 +3,7 @@ import {
consumeRootCommandOptionToken,
getCommandArgsWithRootOptions,
} from "../infra/cli-root-options.js";
import { normalizeFsSafeNativeEnv } from "../infra/fs-safe-env.js";
import type { resolveCliArgvInvocation } from "./argv-invocation.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js";
@ -67,6 +68,7 @@ export async function tryRunUpdateAdmissionBeforeStartup(
if (profile.profile) {
applyCliProfileEnv({ profile: profile.profile });
}
normalizeFsSafeNativeEnv();
const { updateAdmitCommand } = await import("./update-cli/update-command-admit.js");
await updateAdmitCommand(contextPath);
return true;

View file

@ -1,7 +1,20 @@
import { afterEach, expect, it, vi } from "vitest";
import { withUpdateEnv } from "./update-command-service-env.js";
import {
clearFsSafeEnvFallback,
fsSafeEnvInput,
normalizeFsSafeNativeEnv,
} from "../../infra/fs-safe-env.js";
import {
withOwnedManagedUpdateEnv,
withUpdateEnv,
withUpdateInProgressEnv,
} from "./update-command-service-env.js";
afterEach(() => vi.unstubAllEnvs());
afterEach(() => {
clearFsSafeEnvFallback(process.env);
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it.each([false, true])("restores only update phase overrides after failure=%s", async (fails) => {
vi.stubEnv("OPENCLAW_UPDATE_IN_PROGRESS", "previous");
@ -36,3 +49,47 @@ it.each([false, true])("restores only update phase overrides after failure=%s",
expect(process.env.OPENCLAW_UPDATE_TEST_NEW).toBeUndefined();
expect(process.env.OPENCLAW_UPDATE_TEST_OTHER).toBe("phase-owned");
});
it("keeps derived legacy modes below native update overrides and restores their provenance", async () => {
vi.stubEnv("FS_SAFE_NATIVE_MODE", undefined);
vi.stubEnv("OPENCLAW_FS_SAFE_NATIVE_MODE", undefined);
vi.stubEnv("FS_SAFE_PYTHON_MODE", "require");
vi.stubEnv("OPENCLAW_FS_SAFE_PYTHON_MODE", undefined);
vi.spyOn(process, "emitWarning").mockImplementation(() => {});
normalizeFsSafeNativeEnv();
await withUpdateEnv({ OPENCLAW_FS_SAFE_NATIVE_MODE: "off" }, async () => {
expect(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("off");
});
expect(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("require");
expect(fsSafeEnvInput(process.env).OPENCLAW_FS_SAFE_NATIVE_MODE).toBeUndefined();
await withOwnedManagedUpdateEnv({ FS_SAFE_PYTHON_MODE: "off" }, async () => {
expect(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("off");
});
expect(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("require");
delete process.env.FS_SAFE_PYTHON_MODE;
normalizeFsSafeNativeEnv();
expect(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBeUndefined();
});
it.skipIf(process.platform !== "win32")(
"restores differently cased Windows selectors after an update with a legacy native mode",
async () => {
vi.stubEnv("FS_SAFE_NATIVE_MODE", undefined);
vi.stubEnv("OPENCLAW_FS_SAFE_NATIVE_MODE", undefined);
vi.stubEnv("FS_SAFE_PYTHON_MODE", "require");
vi.stubEnv("OPENCLAW_FS_SAFE_PYTHON_MODE", undefined);
vi.stubEnv("OPENCLAW_STATE_DIR", undefined);
vi.stubEnv("openclaw_state_dir", "./previous-state");
vi.spyOn(process, "emitWarning").mockImplementation(() => {});
normalizeFsSafeNativeEnv();
await withUpdateInProgressEnv("C:\\update-fixture", async () => {
expect(process.env.OPENCLAW_STATE_DIR).toBe("C:\\update-fixture\\previous-state");
expect(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("require");
});
expect(process.env.OPENCLAW_STATE_DIR).toBe("./previous-state");
expect(process.env.openclaw_state_dir).toBe("./previous-state");
expect(fsSafeEnvInput(process.env).OPENCLAW_FS_SAFE_NATIVE_MODE).toBeUndefined();
},
);

View file

@ -3,6 +3,11 @@ import {
GATEWAY_SERVICE_RUNTIME_PID_ENV,
GATEWAY_SERVICE_SELECTOR_ENV_KEYS,
} from "../../daemon/constants.js";
import {
clearFsSafeEnvFallback,
fsSafeEnvInput,
normalizeFsSafeNativeEnv,
} from "../../infra/fs-safe-env.js";
import { mergePathPrepend } from "../../infra/path-prepend.js";
import { mergeProcessEnv, resolveEnvironmentValue } from "../../infra/process-env.js";
import { quoteCliArg, quotePowerShellArg } from "../quote-cli-arg.js";
@ -58,7 +63,7 @@ function applyManagedServiceSelectorEnv(params: {
serviceEnv: NodeJS.ProcessEnv;
selectorEnv?: NodeJS.ProcessEnv;
}): NodeJS.ProcessEnv {
const resolved = { ...params.baseEnv };
const resolved = { ...fsSafeEnvInput(params.baseEnv) };
const selectorEnv = params.selectorEnv ?? params.serviceEnv;
for (const key of MANAGED_UPDATE_SELECTOR_ENV_KEYS) {
if (resolveEnvironmentValue(selectorEnv, key)?.trim()) {
@ -79,9 +84,12 @@ export function resolveServiceRefreshEnv(
const resolvedEnv: NodeJS.ProcessEnv =
process.platform === "win32"
? Object.fromEntries(
Object.entries(mergeProcessEnv([env])).map(([key, value]) => [key.toUpperCase(), value]),
Object.entries(mergeProcessEnv([fsSafeEnvInput(env)])).map(([key, value]) => [
key.toUpperCase(),
value,
]),
)
: { ...env };
: { ...fsSafeEnvInput(env) };
for (const key of SERVICE_REFRESH_PATH_ENV_KEYS) {
const rawValue = resolvedEnv[key]?.trim();
if (!rawValue) {
@ -108,25 +116,29 @@ export async function withOwnedManagedUpdateEnv<T>(
}
// Update finalization is a single serialized CLI phase. Some plugin/config owners still read
// process.env, so switch the complete phase atomically and restore the caller afterward.
const previousEnv = { ...process.env };
const previousEnv = { ...fsSafeEnvInput(process.env) };
// Snapshot an aliased input before clearing the process environment.
const phaseEnv = env === process.env ? previousEnv : { ...fsSafeEnvInput(env) };
clearFsSafeEnvFallback(process.env);
for (const key of Object.keys(process.env)) {
delete process.env[key];
}
// A caller may pass process.env itself; clearing it must not erase the supplied scope.
const phaseEnv = env === process.env ? previousEnv : env;
for (const [key, value] of Object.entries(phaseEnv)) {
// Node stringifies undefined on assignment; unset selectors must remain absent.
if (value !== undefined) {
process.env[key] = value;
}
}
normalizeFsSafeNativeEnv();
try {
return await run();
} finally {
clearFsSafeEnvFallback(process.env);
for (const key of Object.keys(process.env)) {
delete process.env[key];
}
Object.assign(process.env, previousEnv);
normalizeFsSafeNativeEnv();
}
}
@ -135,8 +147,17 @@ export async function withUpdateEnv<T>(
overrides: NodeJS.ProcessEnv,
run: () => Promise<T>,
): Promise<T> {
const previous = Object.keys(overrides).map((key) => [key, process.env[key]] as const);
const inputs = fsSafeEnvInput(overrides);
const before = fsSafeEnvInput(process.env);
const previous = Object.keys(inputs).map(
(key) =>
[
key,
process.platform === "win32" ? resolveEnvironmentValue(before, key) : before[key],
] as const,
);
const apply = (entries: Iterable<readonly [string, string | undefined]>) => {
clearFsSafeEnvFallback(process.env);
for (const [key, value] of entries) {
if (value === undefined) {
delete process.env[key];
@ -144,8 +165,9 @@ export async function withUpdateEnv<T>(
process.env[key] = value;
}
}
normalizeFsSafeNativeEnv();
};
apply(Object.entries(overrides));
apply(Object.entries(inputs));
try {
return await run();
} finally {

View file

@ -1,4 +1,9 @@
import { resolveEnvNormalizationKeys } from "../infra/env.js";
import {
clearFsSafeEnvFallback,
fsSafeEnvInput,
normalizeFsSafeNativeEnv,
} from "../infra/fs-safe-env.js";
import {
isDangerousHostEnvOverrideVarName,
isDangerousHostEnvVarName,
@ -78,7 +83,7 @@ export function snapshotEnvByPlatformKey(
// Windows has one logical slot per case-insensitive key. Retain its exact spelling so
// publication and rollback can compare-and-swap the slot without losing the original key.
const snapshot = new Map<string, EnvSnapshotEntry>();
for (const [key, value] of Object.entries(env)) {
for (const [key, value] of Object.entries(fsSafeEnvInput(env))) {
const platformKey = envSnapshotKey(key);
if (!snapshot.has(platformKey)) {
snapshot.set(platformKey, { key, value });
@ -99,6 +104,7 @@ export function replaceEnvSnapshotEntry(
current: EnvSnapshotEntry | undefined,
next: EnvSnapshotEntry | undefined,
): void {
clearFsSafeEnvFallback(env);
if (current) {
delete env[current.key];
}
@ -124,7 +130,7 @@ export function indexConfigRuntimeEnvValues(
export function snapshotEnvProperties(
env: Readonly<NodeJS.ProcessEnv>,
): Map<string, EnvSnapshotEntry> {
return new Map(Object.entries(env).map(([key, value]) => [key, { key, value }]));
return new Map(Object.entries(fsSafeEnvInput(env)).map(([key, value]) => [key, { key, value }]));
}
export type PublishedConfigRuntimeEnvChange = {
@ -145,4 +151,5 @@ export function rollbackConfigRuntimeEnvChanges(
}
replaceEnvSnapshotEntry(env, currentEntry, change.before);
}
normalizeFsSafeNativeEnv(env);
}

View file

@ -4,6 +4,11 @@ import {
normalizeZaiEnv,
resolveEnvNormalizationKeys,
} from "../infra/env.js";
import {
clearFsSafeEnvFallback,
fsSafeEnvInput,
normalizeFsSafeNativeEnv,
} from "../infra/fs-safe-env.js";
import {
collectConfigRuntimeEnvVars,
envSnapshotEntriesEqual,
@ -70,6 +75,7 @@ export function restoreEnvChangesIfUnchanged(params: {
replaceEnvSnapshotEntry(owned, currentOwned.get(key), beforeOwned.get(key));
}
appliedConfigEnvOwnership.set(params.env, owned);
normalizeFsSafeNativeEnv(params.env);
}
type ConfigReadEnvChanges = {
@ -186,7 +192,7 @@ export function captureConfigReadEnvMutation<T>(
const key = change.after?.key ?? change.before?.key ?? change.key;
const unchanged = change.after
? envSnapshotEntriesEqual(current.get(key), change.after)
: !Object.hasOwn(env, key);
: !current.has(key);
if (!unchanged || !envSnapshotEntriesEqual(currentOwned.get(key), change.afterOwned)) {
continue;
}
@ -197,6 +203,7 @@ export function captureConfigReadEnvMutation<T>(
replaceEnvSnapshotEntry(owned, currentOwned.get(key), change.beforeOwned);
}
appliedConfigEnvOwnership.set(env, owned);
normalizeFsSafeNativeEnv(env);
};
// Snapshot rejection and include compensation consume the same receipt once.
retainRestore?.(restore);
@ -207,7 +214,7 @@ export function captureConfigReadEnvMutation<T>(
}
export function cloneEnvWithPlatformSemantics(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
let cloned: NodeJS.ProcessEnv = { ...env };
let cloned: NodeJS.ProcessEnv = { ...fsSafeEnvInput(env) };
// A plain spread loses Windows process.env's case-insensitive lookup and assignment semantics.
if (process.platform === "win32") {
cloned = new Proxy(cloned, {
@ -257,6 +264,7 @@ export function cloneEnvWithPlatformSemantics(env: NodeJS.ProcessEnv): NodeJS.Pr
});
}
appliedConfigEnvOwnership.set(cloned, resolveAppliedConfigEnvOwnership(env));
normalizeFsSafeNativeEnv(cloned);
return cloned;
}
@ -309,6 +317,8 @@ export function collectConfigRuntimeEnvOwnership(
after: Readonly<Record<string, string | undefined>>,
options: { replacedLowerPrecedenceKeys?: readonly string[] } = {},
): Record<string, string> {
const beforeInput = fsSafeEnvInput(before);
const afterInput = fsSafeEnvInput(after);
const ownedEnv: Record<string, string> = {};
// Equal bytes cannot reveal that config replaced a lower-precedence layer.
// Carry the apply-time replacement signal so later reloads can remove that owned value.
@ -317,14 +327,14 @@ export function collectConfigRuntimeEnvOwnership(
);
for (const [key, value] of Object.entries(collectConfigRuntimeEnvVars(sourceConfig))) {
for (const normalizedKey of resolveEnvNormalizationKeys(key)) {
const afterKey = findCaseInsensitiveEnvKey(after, normalizedKey);
if (!afterKey || after[afterKey] !== value) {
const afterKey = findCaseInsensitiveEnvKey(afterInput, normalizedKey);
if (!afterKey || afterInput[afterKey] !== value) {
continue;
}
const beforeKey = findCaseInsensitiveEnvKey(before, normalizedKey);
const beforeKey = findCaseInsensitiveEnvKey(beforeInput, normalizedKey);
if (
beforeKey &&
before[beforeKey] === value &&
beforeInput[beforeKey] === value &&
!replacedLowerPrecedenceKeys.has(envSnapshotKey(afterKey))
) {
continue;
@ -340,12 +350,13 @@ function filterConfigRuntimeEnvOwnership(
env: NodeJS.ProcessEnv,
ownedEnv: Readonly<Record<string, string>>,
): Record<string, string> {
const input = fsSafeEnvInput(env);
const allowedValues = indexConfigRuntimeEnvValues(collectConfigRuntimeEnvVars(sourceConfig));
const filtered: Record<string, string> = {};
for (const [key, value] of Object.entries(ownedEnv)) {
const normalizedKey = resolveEnvNormalizationKeys(key)[0] ?? key;
const actualKey = findCaseInsensitiveEnvKey(env, key);
if (actualKey && env[actualKey] === value && allowedValues.get(normalizedKey)?.has(value)) {
const actualKey = findCaseInsensitiveEnvKey(input, key);
if (actualKey && input[actualKey] === value && allowedValues.get(normalizedKey)?.has(value)) {
filtered[actualKey] = value;
}
}
@ -398,6 +409,7 @@ export function createConfigRuntimeEnvBase(
} = {},
): NodeJS.ProcessEnv {
const isolated = cloneEnvWithPlatformSemantics(env);
clearFsSafeEnvFallback(isolated);
const ownedEnv = filterConfigRuntimeEnvOwnership(
activeConfig,
env,
@ -411,6 +423,7 @@ export function createConfigRuntimeEnvBase(
delete isolated[key];
}
}
normalizeFsSafeNativeEnv(isolated);
return isolated;
}
@ -427,7 +440,7 @@ export function prepareConfigRuntimeEnv(params: {
targetEnv,
params.previousOwnedEnv ? { ownedEnv: params.previousOwnedEnv } : {},
);
const base = { ...preparedEnv } as Record<string, string | undefined>;
const base = { ...fsSafeEnvInput(preparedEnv) };
applyConfigEnvVars(params.nextConfig, preparedEnv);
const preparedOwnedEnv = collectConfigRuntimeEnvOwnership(params.nextConfig, base, preparedEnv);
@ -521,6 +534,7 @@ function prepareConfigRuntimeEnvPublication(params: {
};
}): PreparedConfigRuntimeEnv {
const { targetEnv, before, preparedEnv } = params;
normalizeFsSafeNativeEnv(preparedEnv);
const afterByPlatformKey = snapshotEnvByPlatformKey(preparedEnv);
return {
@ -563,6 +577,7 @@ function prepareConfigRuntimeEnvPublication(params: {
replaceEnvSnapshotEntry(targetEnv, currentEntry, afterEntry);
}
}
normalizeFsSafeNativeEnv(targetEnv);
const generation = processPublication ? publishedConfigRuntimeEnvState.generation + 1 : null;
let processPublicationState: PendingConfigRuntimeEnvPublication | null = null;
if (generation !== null) {
@ -642,6 +657,7 @@ export function applyConfigEnvVars(
onLowerPrecedenceKeysReplaced?: (keys: readonly string[]) => void;
} = {},
): void {
clearFsSafeEnvFallback(env);
const before = { ...env };
const previousOwnedEnv = resolveAppliedConfigEnvOwnership(env);
const entries = collectConfigRuntimeEnvVars(cfg);
@ -695,4 +711,5 @@ export function applyConfigEnvVars(
...filterConfigRuntimeEnvOwnership(cfg, env, previousOwnedEnv),
...collectConfigRuntimeEnvOwnership(cfg, before, env, { replacedLowerPrecedenceKeys }),
});
normalizeFsSafeNativeEnv(env);
}

View file

@ -0,0 +1,138 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearFsSafeEnvFallback,
fsSafeEnvInput,
normalizeFsSafeNativeEnv,
} from "../infra/fs-safe-env.js";
import {
applyConfigEnvVars,
captureConfigReadEnvMutation,
cloneEnvWithPlatformSemantics,
collectConfigRuntimeEnvOwnership,
getPublishedConfigRuntimeEnvState,
initializePublishedConfigRuntimeEnv,
prepareConfigRuntimeEnv,
resetPublishedConfigRuntimeEnv,
restoreEnvChangesIfUnchanged,
snapshotEnv,
} from "./config-env-vars.js";
import type { OpenClawConfig } from "./types.js";
const nativeKey = "OPENCLAW_FS_SAFE_NATIVE_MODE";
const legacyKey = "FS_SAFE_PYTHON_MODE";
const config = (vars: Record<string, string>): OpenClawConfig => ({ env: { vars } });
beforeEach(() => {
for (const key of [nativeKey, "FS_SAFE_NATIVE_MODE", legacyKey, "OPENCLAW_FS_SAFE_PYTHON_MODE"]) {
vi.stubEnv(key, undefined);
}
vi.spyOn(process, "emitWarning").mockImplementation(() => {});
});
afterEach(() => {
clearFsSafeEnvFallback(process.env);
resetPublishedConfigRuntimeEnv();
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
function initialize(source: OpenClawConfig) {
const before = snapshotEnv(process.env);
applyConfigEnvVars(source);
initializePublishedConfigRuntimeEnv(source, {
ownedEnv: collectConfigRuntimeEnvOwnership(source, before, process.env),
});
}
describe("config-owned fs-safe mode migration", () => {
it("lets a later native config value beat an inherited legacy mode and restores fallback on removal", () => {
process.env[legacyKey] = "off";
normalizeFsSafeNativeEnv();
const source = config({ [nativeKey]: "require" });
initialize(source);
expect(process.env[nativeKey]).toBe("require");
expect(getPublishedConfigRuntimeEnvState().ownedEnv).toEqual({ [nativeKey]: "require" });
const removal = prepareConfigRuntimeEnv({ previousConfig: source, nextConfig: {} }).publish();
removal.commit();
expect(process.env[nativeKey]).toBe("off");
expect(fsSafeEnvInput(process.env)[nativeKey]).toBeUndefined();
expect(getPublishedConfigRuntimeEnvState().ownedEnv).toEqual({});
});
it("owns only the legacy input and removes the derived mode with its config", () => {
const source = config({ [legacyKey]: " Required " });
initialize(source);
expect(process.env[nativeKey]).toBe(" Required ");
expect(getPublishedConfigRuntimeEnvState().ownedEnv).toEqual({ [legacyKey]: " Required " });
const removal = prepareConfigRuntimeEnv({ previousConfig: source, nextConfig: {} }).publish();
expect(process.env[legacyKey]).toBeUndefined();
expect(process.env[nativeKey]).toBeUndefined();
removal();
expect(process.env[legacyKey]).toBe(" Required ");
expect(process.env[nativeKey]).toBe(" Required ");
expect(fsSafeEnvInput(process.env)[nativeKey]).toBeUndefined();
});
it("restores legacy fallback after a rejected native publication without adopting it", () => {
const source = config({ [legacyKey]: "require" });
initialize(source);
const replacement = prepareConfigRuntimeEnv({
previousConfig: source,
nextConfig: config({ [nativeKey]: "off" }),
}).publish();
expect(process.env[nativeKey]).toBe("off");
expect(process.env[legacyKey]).toBeUndefined();
replacement();
expect(process.env[nativeKey]).toBe("require");
expect(getPublishedConfigRuntimeEnvState().ownedEnv).toEqual({ [legacyKey]: "require" });
prepareConfigRuntimeEnv({ previousConfig: source, nextConfig: {} }).publish().commit();
expect(process.env[nativeKey]).toBeUndefined();
});
it("distinguishes an equal-byte native config value from an earlier derived fallback", () => {
process.env[legacyKey] = "off";
normalizeFsSafeNativeEnv();
const source = config({ [nativeKey]: "off" });
initialize(source);
expect(fsSafeEnvInput(process.env)[nativeKey]).toBe("off");
delete process.env[legacyKey];
normalizeFsSafeNativeEnv();
expect(process.env[nativeKey]).toBe("off");
expect(getPublishedConfigRuntimeEnvState().ownedEnv).toEqual({ [nativeKey]: "off" });
});
it("recomputes isolated candidates without changing their parent's legacy projection", () => {
const env: NodeJS.ProcessEnv = { [legacyKey]: "off" };
normalizeFsSafeNativeEnv(env);
const clone = cloneEnvWithPlatformSemantics(env);
applyConfigEnvVars(config({ [nativeKey]: "require" }), clone);
expect(clone[nativeKey]).toBe("require");
expect(env[nativeKey]).toBe("off");
expect(fsSafeEnvInput(env)[nativeKey]).toBeUndefined();
});
it("restores invalid native input after a rejected config read introduced legacy mode", () => {
const env: NodeJS.ProcessEnv = { [nativeKey]: "invalid" };
const before = snapshotEnv(env);
applyConfigEnvVars(config({ [legacyKey]: "require" }), env);
expect(env[nativeKey]).toBe("require");
restoreEnvChangesIfUnchanged({ env, before, after: snapshotEnv(env) });
expect(env).toEqual({ [nativeKey]: "invalid" });
});
it("compensates synchronous read mutations using source keys, preserving later operator changes", () => {
const env: NodeJS.ProcessEnv = {};
let restore: (() => void) | undefined;
captureConfigReadEnvMutation(
env,
() => applyConfigEnvVars(config({ [legacyKey]: "off" }), env),
(receipt) => {
restore = receipt;
},
);
expect(env[nativeKey]).toBe("off");
env.FS_SAFE_NATIVE_MODE = "require";
restore?.();
expect(env).toEqual({ FS_SAFE_NATIVE_MODE: "require" });
});
});

View file

@ -33,6 +33,7 @@ import { installDistEsmResolveFastPath } from "./entry.esm-resolve-fast-path.js"
import { buildCliRespawnPlan, runCliRespawnPlan } from "./entry.respawn.js";
import { tryHandleRootVersionFastPath } from "./entry.version-fast-path.js";
import { normalizeEnv } from "./infra/env.js";
import { fsSafeEnvInput } from "./infra/fs-safe-env.js";
import { isMainModule } from "./infra/is-main.js";
import { ensureOpenClawExecMarkerOnProcess } from "./infra/openclaw-exec-env.js";
import { installProcessWarningFilter } from "./infra/warning-filter.js";
@ -142,7 +143,7 @@ if (
if (earlyProfile.ok && earlyProfile.profile) {
applyCliProfileEnv({ profile: earlyProfile.profile });
}
const startupEnv = { ...process.env };
const startupEnv = { ...fsSafeEnvInput(process.env) };
const { assertSupportedRuntime, isCurrentRuntimeSupported } =
await import("./infra/runtime-guard.js");
if (!(await isCurrentRuntimeSupported())) {

View file

@ -564,7 +564,6 @@ export async function restoreFleetCell(params: {
symlinks: "reject",
hardlinks: "reject",
maxBytes: MANIFEST_MAX_BYTES,
nonBlockingRead: true,
});
let manifest: unknown;
try {

View file

@ -101,7 +101,6 @@ export async function prepareCellConfig(
const cellRoot = await fsSafeRoot(record.dataDir, {
hardlinks: "reject",
maxBytes: CELL_CONFIG_MAX_BYTES,
nonBlockingRead: true,
symlinks: "reject",
});
try {

View file

@ -185,7 +185,6 @@ async function readWorkspaceFileContent(
const workspaceRoot = await root(workspaceDir);
const safeRead = await workspaceRoot.read(name, {
hardlinks: "reject",
nonBlockingRead: true,
});
return safeRead.buffer.toString("utf-8");
} catch (err) {
@ -246,7 +245,6 @@ async function readWorkspaceFileHash(
try {
const safeRead = await workspaceRoot.read(name, {
hardlinks: "reject",
nonBlockingRead: true,
});
return sha256Hex(safeRead.buffer);
} catch (err) {
@ -345,7 +343,6 @@ export const agentFileHandlers: Pick<
const workspaceRoot = await root(workspaceDir);
safeRead = await workspaceRoot.read(name, {
hardlinks: "reject",
nonBlockingRead: true,
});
} catch (err) {
if (isMissingPathError(err)) {

View file

@ -150,7 +150,6 @@ export function registerAgentIdentityUpdateTests(harness: IdentityUpdateHarness)
expectRespondOk(respond, { ok: true, agentId: "test-agent" });
expectRecordFields(mockCallArg(mocks.rootRead), {
relativePath: "IDENTITY.md",
nonBlockingRead: true,
});
const configOptions = expectRecordFields(mockCallArg(mocks.applyAgentConfig, 0, 1), {
name: "New Name",

View file

@ -2080,7 +2080,6 @@ describe("agents.files.get/set symlink safety", () => {
rootDir: "/workspace/test-agent",
relativePath: "AGENTS.md",
hardlinks: "reject",
nonBlockingRead: true,
});
const payload = expectRespondOk(respond, {});
expectRecordFields(payload.file, {

View file

@ -181,7 +181,6 @@ async function runPersonalFile(
mutationSymlinks: "reject",
hardlinks: "reject",
maxBytes: MAX_WORKSPACE_BOOTSTRAP_FILE_BYTES,
nonBlockingRead: true,
assertBeforeMutation: target.assertCurrent,
});
const read = async (): Promise<UsersPersonalFileGetResult> => {

View file

@ -46,7 +46,6 @@ export async function openWorkspaceRoot(rootDir: string): Promise<WorkspaceRoot
return await fsSafeRoot(rootDir, {
hardlinks: "reject",
maxBytes: WORKSPACE_PREVIEW_MAX_BYTES,
nonBlockingRead: true,
symlinks: "reject",
});
} catch {

View file

@ -20,7 +20,6 @@ export async function copyNodeBootstrapPrebuiltArchive(packageRoot: string, temp
const source = await openFsRoot(packageRoot, {
symlinks: "reject",
hardlinks: "allow",
nonBlockingRead: true,
});
const destination = await openFsRoot(temporaryRoot);
try {

View file

@ -35,7 +35,6 @@ export async function resolveNodeBootstrapRuntimeChunks(packageRoot: string, fil
const root = await openFsRoot(packageRoot, {
hardlinks: "allow",
symlinks: "reject",
nonBlockingRead: true,
});
const fileSet = new Set(files);
const imports = new Map<string, string[]>();

View file

@ -5,6 +5,7 @@ import path from "node:path";
import { readRegularFile, readRegularFileSync } from "@openclaw/fs-safe/advanced";
import { parse as parseDotEnv } from "dotenv";
import { resolveConfigDir } from "./config-dir.js";
import { clearFsSafeEnvFallback, normalizeFsSafeNativeEnv } from "./fs-safe-env.js";
import { resolveRequiredHomeDir } from "./home-dir.js";
import { normalizeEnvVarKey } from "./host-env-security.js";
@ -112,6 +113,7 @@ function loadParsedDotEnvFiles(
overrideKeys?: Iterable<string>,
onWarning?: DotEnvWarning,
): Map<string, string[]> {
clearFsSafeEnvFallback(env);
const preExistingKeys = new Set(Object.keys(env));
const canonicalizeKey = (key: string): string | null =>
normalizeEnvVarKey(key, { portable: true })?.toUpperCase() ?? null;
@ -174,6 +176,7 @@ function loadParsedDotEnvFiles(
}
}
normalizeFsSafeNativeEnv(env);
for (const conflict of conflicts.values()) {
const keys = [...conflict.keys].toSorted();
onWarning?.(

View file

@ -2,7 +2,12 @@ import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
loadGlobalRuntimeDotEnvFilesAsyncCore,
loadGlobalRuntimeDotEnvFilesCore,
} from "./dotenv-global-core.js";
import { readDotEnvFile } from "./dotenv-global.js";
import { fsSafeEnvInput } from "./fs-safe-env.js";
const logWarnSpy = vi.hoisted(() => vi.fn());
@ -77,3 +82,25 @@ describe("readDotEnvFile", () => {
);
});
});
it.each(["sync", "async"] as const)(
"maps legacy modes from %s dotenv loads without blocking a later native setting",
async (mode) => {
const home = tmpDir();
const stateEnvPath = join(home, "runtime.env");
const env: NodeJS.ProcessEnv = { HOME: home, OPENCLAW_STATE_DIR: home };
const load = () =>
mode === "sync"
? loadGlobalRuntimeDotEnvFilesCore({ env, stateEnvPath })
: loadGlobalRuntimeDotEnvFilesAsyncCore({ env, stateEnvPath });
writeFileSync(stateEnvPath, "FS_SAFE_PYTHON_MODE=require\n");
const initial = await load();
expect(env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("require");
expect(initial.stateEnvAppliedKeys).toEqual(["FS_SAFE_PYTHON_MODE"]);
expect(fsSafeEnvInput(env).OPENCLAW_FS_SAFE_NATIVE_MODE).toBeUndefined();
writeFileSync(stateEnvPath, "OPENCLAW_FS_SAFE_NATIVE_MODE=off\n");
await load();
expect(env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("off");
expect(fsSafeEnvInput(env).OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("off");
},
);

View file

@ -10,6 +10,7 @@ import {
readDotEnvFile,
readDotEnvFileAsync,
} from "./dotenv-global.js";
import { clearFsSafeEnvFallback, normalizeFsSafeNativeEnv } from "./fs-safe-env.js";
import {
isDangerousHostEnvOverrideVarName,
isDangerousHostEnvVarName,
@ -268,12 +269,14 @@ export function loadWorkspaceDotEnvFile(
if (!parsed) {
return;
}
clearFsSafeEnvFallback(env);
for (const { key, value } of parsed.entries) {
if (env[key] !== undefined) {
continue;
}
env[key] = value;
}
normalizeFsSafeNativeEnv(env);
}
async function loadWorkspaceDotEnvFileAsync(
@ -294,11 +297,13 @@ async function loadWorkspaceDotEnvFileAsync(
includeUntrustedWorkspacePlugins: false,
}),
);
clearFsSafeEnvFallback(opts.env);
for (const { key, value } of parsed.entries) {
if (!blocked.has(key.toUpperCase()) && opts.env[key] === undefined) {
opts.env[key] = value;
}
}
normalizeFsSafeNativeEnv(opts.env);
}
export async function loadDotEnvAsync(opts: {

View file

@ -2,6 +2,7 @@
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { createLazyPromise } from "../shared/lazy-runtime.js";
import { parseBooleanValue } from "../utils/boolean.js";
import { normalizeFsSafeNativeEnv } from "./fs-safe-env.js";
export { isFastTestRuntimeEnv, isVitestRuntimeEnv } from "./test-runtime-env.js";
const loadLog = createLazyPromise(
@ -91,4 +92,5 @@ export function isTruthyEnvValue(value?: string): boolean {
/** Applies process-wide env normalization before runtime configuration is read. */
export function normalizeEnv(): void {
normalizeZaiEnv(process.env);
normalizeFsSafeNativeEnv(process.env);
}

View file

@ -0,0 +1,78 @@
import type { Root as FsSafeRoot } from "@openclaw/fs-safe/root";
import type { FileStore as FsSafeFileStore } from "@openclaw/fs-safe/store";
import {
tempWorkspace as fsSafeTempWorkspace,
withTempWorkspace as fsSafeWithTempWorkspace,
type TempWorkspace as FsSafeTempWorkspace,
} from "@openclaw/fs-safe/temp";
export type LegacyNonBlockingReadOption = {
/** @deprecated Omit this hint; safe reads always use nonblocking admission where supported. */
nonBlockingRead?: boolean;
};
type WithLegacyReadHint<Method> = Method extends (
path: infer Path,
options?: infer Options,
) => infer Result
? (path: Path, options?: NonNullable<Options> & LegacyNonBlockingReadOption) => Result
: never;
type CompatibleRootReadMethods = {
[Method in "open" | "read" | "readBytes" | "readText" | "readAbsolute"]: WithLegacyReadHint<
FsSafeRoot[Method]
>;
};
// Keep the full vendor Root here; individual SDK owners retain their existing capability limits.
export type CompatibleFsSafeRoot = Omit<
FsSafeRoot,
keyof CompatibleRootReadMethods | "defaults" | "readJson" | "reader"
> &
CompatibleRootReadMethods & {
readonly defaults: FsSafeRoot["defaults"] & LegacyNonBlockingReadOption;
readJson<T = unknown>(
path: Parameters<FsSafeRoot["readJson"]>[0],
options?: NonNullable<Parameters<FsSafeRoot["readJson"]>[1]> & LegacyNonBlockingReadOption,
): Promise<T>;
reader(
options?: NonNullable<Parameters<FsSafeRoot["reader"]>[0]> & LegacyNonBlockingReadOption,
): ReturnType<FsSafeRoot["reader"]>;
};
type CompatibleStoreReadMethods = {
[Method in "open" | "read" | "readBytes" | "readText" | "readTextIfExists"]: WithLegacyReadHint<
FsSafeFileStore[Method]
>;
};
type CompatibleFileStore = Omit<
FsSafeFileStore,
keyof CompatibleStoreReadMethods | "root" | "readJson" | "readJsonIfExists"
> &
CompatibleStoreReadMethods & {
root(): Promise<CompatibleFsSafeRoot>;
readJson<T = unknown>(
path: Parameters<FsSafeFileStore["readJson"]>[0],
options?: NonNullable<Parameters<FsSafeFileStore["readJson"]>[1]> &
LegacyNonBlockingReadOption,
): Promise<T>;
readJsonIfExists<T = unknown>(
path: Parameters<FsSafeFileStore["readJsonIfExists"]>[0],
options?: NonNullable<Parameters<FsSafeFileStore["readJsonIfExists"]>[1]> &
LegacyNonBlockingReadOption,
): Promise<T | null>;
};
export type CompatibleTempWorkspace = Omit<FsSafeTempWorkspace, "store"> & {
store: CompatibleFileStore;
};
// Preserve the shipped async workspace options without wrapping native operations.
export const tempWorkspace: (
options: Parameters<typeof fsSafeTempWorkspace>[0],
) => Promise<CompatibleTempWorkspace> = fsSafeTempWorkspace;
export const withTempWorkspace: <T>(
options: Parameters<typeof fsSafeTempWorkspace>[0],
run: (workspace: CompatibleTempWorkspace) => Promise<T>,
) => Promise<T> = fsSafeWithTempWorkspace;

View file

@ -13,6 +13,7 @@ import type {
FsSafeCopyReply,
FsSafeCopyWrite,
} from "./fs-safe-copy-worker-contract.js";
import { normalizeFsSafeNativeEnv } from "./fs-safe-env.js";
function failure(error: unknown): FsSafeCopyReply {
return {
@ -24,6 +25,8 @@ function failure(error: unknown): FsSafeCopyReply {
};
}
normalizeFsSafeNativeEnv();
if (parentPort) {
// This isolate uses the library's default and explicit operator environment.
// Shared worker plumbing may load Gateway defaults; keep those in the host.

View file

@ -6,6 +6,7 @@ import { nativeProcessTestEntrypoints } from "./native-process-runtime.test-supp
import { resolveRuntimeWorkerArgv, resolveRuntimeWorkerUrl } from "./runtime-worker-url.js";
const coreUrl = resolveRuntimeWorkerUrl(nativeProcessTestEntrypoints.fsSafeCore);
const envUrl = resolveRuntimeWorkerUrl(nativeProcessTestEntrypoints.fsSafeEnv);
const memoryUrl = resolveRuntimeWorkerUrl(nativeProcessTestEntrypoints.memoryFsUtils);
type NativeMode = "auto" | "off" | "require";
@ -35,6 +36,8 @@ function inspectNativeDefaults(params: {
const options = JSON.parse(process.argv[1]);
const config = await import("@openclaw/fs-safe/config");
if (options.beforeImport) config.configureFsSafeNative({ mode: options.beforeImport });
const { normalizeFsSafeNativeEnv } = await import(options.envUrl);
normalizeFsSafeNativeEnv();
await import(options.coreUrl);
await import(options.memoryUrl);
const before = config.getFsSafeNativeConfig().mode;
@ -45,6 +48,7 @@ function inspectNativeDefaults(params: {
JSON.stringify({
...params,
coreUrl: coreUrl.href,
envUrl: envUrl.href,
memoryUrl: memoryUrl.href,
}),
],
@ -103,12 +107,19 @@ describe("fs-safe defaults", () => {
).toEqual({ before: "off", after: "require" });
});
it("retains legacy mode migration without overriding it", () => {
expect(inspectNativeDefaults({ env: { OPENCLAW_FS_SAFE_PYTHON_MODE: "require" } })).toEqual({
before: "require",
after: "require",
});
});
it.each(["FS_SAFE_PYTHON_MODE", "OPENCLAW_FS_SAFE_PYTHON_MODE"])(
"maps deprecated %s at OpenClaw startup without overriding native configuration",
(key) => {
expect(inspectNativeDefaults({ env: { [key]: "require" }, afterImport: "off" })).toEqual({
before: "require",
after: "off",
});
expect(inspectNativeDefaults({ env: { [key]: "require" }, beforeImport: "off" })).toEqual({
before: "off",
after: "off",
});
},
);
it.skipIf(process.platform !== "win32")(
"honors case-insensitive Windows environment names",

View file

@ -0,0 +1,87 @@
import { describe, expect, it } from "vitest";
import { clearFsSafeEnvFallback, fsSafeEnvInput, normalizeFsSafeNativeEnv } from "./fs-safe-env.js";
describe("deprecated fs-safe environment modes", () => {
it.each(["FS_SAFE_PYTHON_MODE", "OPENCLAW_FS_SAFE_PYTHON_MODE"])(
"preserves raw %s values and retires its derived fallback",
(key) => {
const env: NodeJS.ProcessEnv = { [key]: " Required " };
normalizeFsSafeNativeEnv(env);
expect(env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe(" Required ");
expect(fsSafeEnvInput(env)).toEqual({ [key]: " Required " });
delete env[key];
normalizeFsSafeNativeEnv(env);
expect(env).toEqual({});
},
);
it.each(["FS_SAFE_NATIVE_MODE", "OPENCLAW_FS_SAFE_NATIVE_MODE"])(
"matches fs-safe's exact native mode normalization in %s",
(key) => {
for (const value of [
"0",
"false",
" OFF ",
"never",
"1",
"true",
"on",
"auto",
"required",
"require",
]) {
const env = { [key]: value, FS_SAFE_PYTHON_MODE: "require" };
normalizeFsSafeNativeEnv(env);
expect(env).toEqual({ [key]: value, FS_SAFE_PYTHON_MODE: "require" });
}
const env: NodeJS.ProcessEnv = { [key]: "fal\u017fe", FS_SAFE_PYTHON_MODE: "require" };
normalizeFsSafeNativeEnv(env);
expect(env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("require");
},
);
it.each(["", "invalid", "off"])("keeps first-defined legacy precedence for %j", (value) => {
const env: NodeJS.ProcessEnv = {
FS_SAFE_PYTHON_MODE: value,
OPENCLAW_FS_SAFE_PYTHON_MODE: "require",
};
normalizeFsSafeNativeEnv(env);
expect(env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe(value);
});
it.each([undefined, "invalid"])(
"restores an existing native input %j when the legacy fallback disappears",
(previous) => {
const env: NodeJS.ProcessEnv = {
OPENCLAW_FS_SAFE_NATIVE_MODE: previous,
FS_SAFE_PYTHON_MODE: "off",
};
normalizeFsSafeNativeEnv(env);
expect(env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("off");
delete env.FS_SAFE_PYTHON_MODE;
normalizeFsSafeNativeEnv(env);
expect(env).toStrictEqual({ OPENCLAW_FS_SAFE_NATIVE_MODE: previous });
},
);
it("keeps case-sensitive environment slots distinct when deriving a native mode", () => {
const input = { openclaw_fs_safe_native_mode: "invalid", FS_SAFE_PYTHON_MODE: "require" };
const env: NodeJS.ProcessEnv = { ...input };
normalizeFsSafeNativeEnv(env);
expect(env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("require");
expect(env.openclaw_fs_safe_native_mode).toBe("invalid");
expect(fsSafeEnvInput(env)).toStrictEqual(input);
clearFsSafeEnvFallback(env);
expect(env).toStrictEqual(input);
});
it("preserves a later real native write while retiring old fallback provenance", () => {
const env: NodeJS.ProcessEnv = { FS_SAFE_PYTHON_MODE: "require" };
normalizeFsSafeNativeEnv(env);
env.OPENCLAW_FS_SAFE_NATIVE_MODE = "off";
clearFsSafeEnvFallback(env);
expect(env.OPENCLAW_FS_SAFE_NATIVE_MODE).toBe("off");
normalizeFsSafeNativeEnv(env);
expect(fsSafeEnvInput(env)).toBe(env);
});
});

95
src/infra/fs-safe-env.ts Normal file
View file

@ -0,0 +1,95 @@
const nativeModeKey = "OPENCLAW_FS_SAFE_NATIVE_MODE";
const legacyModeKeys = ["FS_SAFE_PYTHON_MODE", "OPENCLAW_FS_SAFE_PYTHON_MODE"] as const;
const legacyKeys = [
...legacyModeKeys,
"FS_SAFE_PYTHON",
"OPENCLAW_FS_SAFE_PYTHON",
"OPENCLAW_PINNED_PYTHON",
"OPENCLAW_PINNED_WRITE_PYTHON",
] as const;
type NativeModeFallback = {
key: string;
value: string;
previous: string | undefined;
existed: boolean;
};
// This records only a derived env slot. Config/dotenv owners retain authority over
// the original inputs, and their snapshots must not adopt the derived value.
const fallbacks = new WeakMap<NodeJS.ProcessEnv, NativeModeFallback>();
let warned = false;
function nativeModeIsValid(value: string | undefined): boolean {
const normalized = value?.trim().toLowerCase() ?? "";
return /^(?:0|false|off|never|1|true|on|auto|required|require)$/u.test(normalized);
}
function currentFallback(env: NodeJS.ProcessEnv): NativeModeFallback | undefined {
const fallback = fallbacks.get(env);
return fallback && env[fallback.key] === fallback.value ? fallback : undefined;
}
/** Read the operator/config inputs without promoting a derived native fallback. */
export function fsSafeEnvInput(env: Readonly<NodeJS.ProcessEnv>): Readonly<NodeJS.ProcessEnv> {
const fallback = currentFallback(env);
if (!fallback) {
return env;
}
const input = { ...env };
if (!fallback.existed) {
delete input[fallback.key];
} else {
input[fallback.key] = fallback.previous;
}
return input;
}
/** Retire our unchanged projection before an environment owner writes real inputs. */
export function clearFsSafeEnvFallback(env: NodeJS.ProcessEnv): void {
const fallback = currentFallback(env);
fallbacks.delete(env);
if (!fallback) {
return;
}
if (!fallback.existed) {
delete env[fallback.key];
} else {
env[fallback.key] = fallback.previous;
}
}
/** Preserve OpenClaw's retired Python mode env contract below both native names. */
export function normalizeFsSafeNativeEnv(env: NodeJS.ProcessEnv = process.env): void {
clearFsSafeEnvFallback(env);
const configured = legacyKeys.filter((key) => env[key] !== undefined);
if (configured.length && env === process.env && !warned) {
warned = true;
process.emitWarning(
`${configured.join(", ")} is deprecated. OpenClaw maps legacy mode values to native ` +
"mode only when neither FS_SAFE_NATIVE_MODE nor OPENCLAW_FS_SAFE_NATIVE_MODE selects " +
"a mode. Replace Python mode variables with native mode variables; interpreter " +
"path settings are ignored.",
{ code: "FS_SAFE_PYTHON_DEPRECATED", type: "DeprecationWarning" },
);
}
if (nativeModeIsValid(env.FS_SAFE_NATIVE_MODE) || nativeModeIsValid(env[nativeModeKey])) {
return;
}
// fs-safe's retired bridge chose the first defined legacy name, even if blank
// or invalid. Keep its raw bytes so config ownership and rollback stay exact.
const value = legacyModeKeys.map((key) => env[key]).find((entry) => entry !== undefined);
if (value === undefined) {
return;
}
const keys = Object.keys(env);
// Only use a differently cased slot when this object actually aliases it.
// Windows Worker environments and plain objects remain case-sensitive.
const key =
!keys.includes(nativeModeKey) && Object.hasOwn(env, nativeModeKey)
? (keys.find((candidate) => candidate.toUpperCase() === nativeModeKey) ?? nativeModeKey)
: nativeModeKey;
const existed = Object.hasOwn(env, key);
const previous = env[key];
env[key] = value;
fallbacks.set(env, { key, value, previous, existed });
}

View file

@ -1,15 +1,15 @@
// Re-exports fs-safe helpers with OpenClaw defaults and wrappers.
import fs from "node:fs/promises";
import path from "node:path";
import { ensureDirectoryWithinRoot, findExistingAncestor } from "@openclaw/fs-safe/advanced";
import {
ensureDirectoryWithinRoot,
findExistingAncestor,
readLocalFileFromRoots as readFsSafeLocalFileFromRoots,
} from "@openclaw/fs-safe/advanced";
import "@openclaw/fs-safe/errors";
import { writeExternalFileWithinRoot as writeExternalFileWithinRootBase } from "@openclaw/fs-safe/output";
import {
root as fsSafeRoot,
type ReadResult,
type Root as FsSafeRoot,
type RootDefaults,
} from "@openclaw/fs-safe/root";
import { root as fsSafeRoot, type ReadResult, type RootDefaults } from "@openclaw/fs-safe/root";
import type { CompatibleFsSafeRoot, LegacyNonBlockingReadOption } from "./fs-safe-compat.js";
export { FsSafeError, type FsSafeErrorCode } from "@openclaw/fs-safe/errors";
export {
@ -27,7 +27,7 @@ export {
export { isPathInside } from "@openclaw/fs-safe/path";
export { pathExists, pathExistsSync } from "@openclaw/fs-safe/advanced";
export { movePathToTrash, type MovePathToTrashOptions } from "@openclaw/fs-safe/advanced";
export { readLocalFileFromRoots, resolveLocalPathFromRootsSync } from "@openclaw/fs-safe/advanced";
export { resolveLocalPathFromRootsSync } from "@openclaw/fs-safe/advanced";
export {
appendRegularFile,
appendRegularFileSync,
@ -59,11 +59,17 @@ export {
} from "@openclaw/fs-safe/walk";
export { withTimeout } from "@openclaw/fs-safe/advanced";
// The broad Plugin SDK infra barrel re-exports this facade. Keep fs-safe 0.5's
// new Root.walk capability core-only until a dedicated plugin contract is approved.
export type Root = Omit<FsSafeRoot, "walk">;
// Root.walk remains core-only on this facade; temp workspace stores keep their shipped full Root.
export type Root = Omit<CompatibleFsSafeRoot, "walk">;
export async function root(rootDir: string, defaults?: RootDefaults): Promise<Root> {
export const readLocalFileFromRoots: (
options: Parameters<typeof readFsSafeLocalFileFromRoots>[0] & LegacyNonBlockingReadOption,
) => ReturnType<typeof readFsSafeLocalFileFromRoots> = readFsSafeLocalFileFromRoots;
export async function root(
rootDir: string,
defaults?: RootDefaults & LegacyNonBlockingReadOption,
): Promise<Root> {
return await fsSafeRoot(rootDir, defaults);
}
@ -134,6 +140,7 @@ export async function readFileWithinRoot(params: {
rootDir: string;
relativePath: string;
rejectHardlinks?: boolean;
/** @deprecated Omit this hint; safe reads always use nonblocking admission where supported. */
nonBlockingRead?: boolean;
allowSymlinkTargetWithinRoot?: boolean;
maxBytes?: number;
@ -142,7 +149,6 @@ export async function readFileWithinRoot(params: {
return await fsRoot.read(params.relativePath, {
hardlinks: params.rejectHardlinks === false ? "allow" : "reject",
maxBytes: params.maxBytes,
nonBlockingRead: params.nonBlockingRead,
symlinks: params.allowSymlinkTargetWithinRoot === true ? "follow-within-root" : "reject",
});
}

View file

@ -20,6 +20,11 @@ export const nativeProcessTestEntrypoints = {
sourceWorkerName: "fs-safe",
distWorkerPath: "infra/fs-safe.js",
},
fsSafeEnv: {
currentModuleUrl: import.meta.url,
sourceWorkerName: "fs-safe-env",
distWorkerPath: "infra/fs-safe-env.js",
},
memoryFsUtils: {
currentModuleUrl: import.meta.url,
sourceWorkerName: "../../packages/memory-host-sdk/src/host/fs-utils",

View file

@ -313,7 +313,6 @@ async function openPackageDistFsRootIfPresent(
): Promise<PackageDistFsRoot | null> {
const packageFs = await openFsRoot(packageRoot, {
hardlinks: "allow",
nonBlockingRead: true,
symlinks: "reject",
});
let distStats;
@ -343,7 +342,6 @@ async function readPackageDistJsonIfExists<T>(
return await packageFs.readJson<T>(relativePath, {
hardlinks: "allow",
maxBytes: 16 * 1024 * 1024,
nonBlockingRead: true,
symlinks: "reject",
});
} catch (error) {
@ -374,7 +372,6 @@ export async function collectPackageDistContentInventory(
fsLimit(async () => {
await using opened = await packageFs.open(relativePath, {
hardlinks: "allow",
nonBlockingRead: true,
symlinks: "reject",
});
return createPackageDistContentInventoryEntry(

View file

@ -190,7 +190,6 @@ export async function captureLocalPackageOverrides(params: {
const baseline = await readPackageDistContentInventoryIfPresent(params.packageRoot);
const packageFs = await openFsRoot(params.packageRoot, {
hardlinks: "reject",
nonBlockingRead: true,
symlinks: "reject",
});

View file

@ -23,7 +23,6 @@ export async function preflightLocalOverrides(params: {
);
const packageFs = await openFsRoot(params.packageRoot, {
hardlinks: "reject",
nonBlockingRead: true,
symlinks: "reject",
});
const conflicts: LocalPackageOverridesResult["conflicts"] = [];

View file

@ -230,7 +230,6 @@ export async function inspectLocalOverrideTarget(params: {
const target = await params.packageFs.read(params.relativePath, {
hardlinks: "reject",
maxBytes: params.expectedSize,
nonBlockingRead: true,
symlinks: "reject",
});
return {

View file

@ -68,7 +68,6 @@ export async function withNodeWorkerUploadSnapshot<T>(
async (workspace) => {
const sourceRoot = await root(params.workspaceDir, {
hardlinks: "allow",
nonBlockingRead: true,
symlinks: "follow-parents-within-root",
});
const stagedRoot = await workspace.store.root();

View file

@ -7,18 +7,50 @@ import { loadSecretFileSync as loadSecretFileSyncFromCore } from "openclaw/plugi
import {
fileExists,
readFileWithinRoot,
readLocalFileFromRoots,
removePathWithinRoot,
root as openRoot,
writeFileWithinRoot,
} from "openclaw/plugin-sdk/file-access-runtime";
import type {
TempWorkspace as SandboxTempWorkspace,
tempWorkspace as sandboxTempWorkspace,
withTempWorkspace as sandboxWithTempWorkspace,
} from "openclaw/plugin-sdk/sandbox";
import {
loadSecretFileSync,
type SecretFileReadResult,
} from "openclaw/plugin-sdk/secret-file-runtime";
import { fileExists as fileExistsFromSecurity } from "openclaw/plugin-sdk/security-runtime";
import { describe, expect, expectTypeOf, it } from "vitest";
import {
fileExists as fileExistsFromSecurity,
replaceFileAtomic,
} from "openclaw/plugin-sdk/security-runtime";
import {
tempWorkspace,
withTempWorkspace,
type TempWorkspace,
} from "openclaw/plugin-sdk/temp-path";
import { describe, expect, expectTypeOf, it, vi } from "vitest";
import { withTestDir } from "../test-helpers/temp-dir.js";
describe("plugin SDK fs-safe compatibility exports", () => {
it("accepts the legacy atomic adapter chmod member without calling it", async () => {
await withTestDir({ prefix: "openclaw-sdk-atomic-compat-" }, async (root) => {
const filePath = path.join(root, "state.txt");
const chmod = vi.fn(async () => {
throw new Error("pathname chmod must remain unused");
});
await replaceFileAtomic({
filePath,
content: "saved",
mode: 0o600,
fileSystem: { promises: { ...fs.promises, chmod } },
});
expect(fs.readFileSync(filePath, "utf8")).toBe("saved");
expect(chmod).not.toHaveBeenCalled();
});
});
it.each([
{ subpath: "file-access-runtime", exists: fileExists },
{ subpath: "security-runtime", exists: fileExistsFromSecurity },
@ -86,13 +118,16 @@ describe("plugin SDK fs-safe compatibility exports", () => {
mkdir: true,
});
const result = await readFileWithinRoot({
rootDir: root,
relativePath: "nested/file.txt",
});
for (const nonBlockingRead of [undefined, true, false]) {
const result = await readFileWithinRoot({
rootDir: root,
relativePath: "nested/file.txt",
nonBlockingRead,
});
expect(result.buffer.toString("utf8")).toBe("hello");
expect(result.realPath).toBe(fs.realpathSync(path.join(root, "nested", "file.txt")));
expect(result.buffer.toString("utf8")).toBe("hello");
expect(result.realPath).toBe(fs.realpathSync(path.join(root, "nested", "file.txt")));
}
await removePathWithinRoot({
rootDir: root,
@ -103,4 +138,102 @@ describe("plugin SDK fs-safe compatibility exports", () => {
expect(fs.existsSync(path.join(root, "nested", "file.txt"))).toBe(false);
});
});
it("keeps legacy hints on local-root reads and transitive temp workspace stores", async () => {
expectTypeOf<typeof sandboxTempWorkspace>().toEqualTypeOf<typeof tempWorkspace>();
expectTypeOf<typeof sandboxWithTempWorkspace>().toEqualTypeOf<typeof withTempWorkspace>();
expectTypeOf<SandboxTempWorkspace>().toEqualTypeOf<TempWorkspace>();
expectTypeOf<
"walk" extends keyof Awaited<ReturnType<typeof openRoot>> ? true : false
>().toEqualTypeOf<false>();
await withTestDir({ prefix: "openclaw-sdk-temp-read-compat-" }, async (rootDir) => {
await using workspace = await tempWorkspace({ rootDir, prefix: "read-" });
await workspace.writeText("data.json", '{"ok":true}');
const filePath = workspace.path("data.json");
for (const nonBlockingRead of [undefined, true, false]) {
expect(
(
await readLocalFileFromRoots({
filePath,
roots: [workspace.dir],
nonBlockingRead,
})
)?.buffer.toString(),
).toBe('{"ok":true}');
const opened = await workspace.store.open("data.json", { nonBlockingRead });
await opened.handle.close();
expect(
(await workspace.store.read("data.json", { nonBlockingRead })).buffer.toString(),
).toBe('{"ok":true}');
expect((await workspace.store.readBytes("data.json", { nonBlockingRead })).toString()).toBe(
'{"ok":true}',
);
expect(
await workspace.store.readText("data.json", { nonBlockingRead, encoding: "utf8" }),
).toBe('{"ok":true}');
expect(
await workspace.store.readTextIfExists("missing.json", { nonBlockingRead }),
).toBeNull();
const parsed = await workspace.store.readJson<{ ok: boolean }>("data.json", {
nonBlockingRead,
});
expectTypeOf(parsed).toEqualTypeOf<{ ok: boolean }>();
expect(parsed).toEqual({ ok: true });
const missing = await workspace.store.readJsonIfExists<{ ok: boolean }>("missing.json", {
nonBlockingRead,
});
expectTypeOf(missing).toEqualTypeOf<{ ok: boolean } | null>();
expect(missing).toBeNull();
const nestedRoot = await workspace.store.root();
expectTypeOf(nestedRoot.walk).toBeFunction();
expect(await nestedRoot.readText("data.json", { nonBlockingRead })).toBe('{"ok":true}');
}
await expect(
withTempWorkspace({ rootDir, prefix: "callback-" }, async (borrowed) => {
await borrowed.writeText("callback.txt", "callback");
return borrowed.store.readText("callback.txt", { nonBlockingRead: false });
}),
).resolves.toBe("callback");
});
});
it("keeps legacy read hints compatible across the SDK Root surface", async () => {
await withTestDir({ prefix: "openclaw-sdk-root-read-compat-" }, async (root) => {
const filePath = path.join(root, "data.json");
const content = '{"ok":true}\n';
fs.writeFileSync(filePath, content);
const scoped = await openRoot(root, { nonBlockingRead: false });
expect(scoped.defaults.nonBlockingRead).toBe(false);
const opened = await scoped.open("data.json", { nonBlockingRead: true });
try {
expect(await opened.handle.readFile({ encoding: "utf8" })).toBe(content);
} finally {
await opened.handle.close();
}
expect((await scoped.read("data.json", { nonBlockingRead: false })).buffer).toEqual(
Buffer.from(content),
);
expect(await scoped.readBytes("data.json", { nonBlockingRead: true })).toEqual(
Buffer.from(content),
);
expect(await scoped.readText("data.json", { nonBlockingRead: false, encoding: "utf8" })).toBe(
content,
);
expect((await scoped.readAbsolute(filePath, { nonBlockingRead: true })).buffer).toEqual(
Buffer.from(content),
);
expect(await scoped.reader({ nonBlockingRead: false })(filePath)).toEqual(
Buffer.from(content),
);
const parsed = await scoped.readJson<{ ok: boolean }>("data.json", { nonBlockingRead: true });
expectTypeOf(parsed).toEqualTypeOf<{ ok: boolean }>();
expect(parsed).toEqual({ ok: true });
await expect(
scoped.read("data.json", { nonBlockingRead: false, maxBytes: 1 }),
).rejects.toMatchObject({ code: "too-large" });
});
});
});

View file

@ -68,14 +68,17 @@ export {
} from "./run-command.js";
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
export {
tempWorkspace,
tempWorkspaceSync,
type TempWorkspace,
type TempWorkspaceOptions,
type TempWorkspaceSync,
withTempWorkspace,
withTempWorkspaceSync,
} from "@openclaw/fs-safe/temp";
export {
tempWorkspace,
withTempWorkspace,
type CompatibleTempWorkspace as TempWorkspace,
} from "../infra/fs-safe-compat.js";
export { SandboxRuntimeRetiredError } from "../agents/sandbox/provisioning-error.js";
export {
createRemoteShellSandboxBackend,

View file

@ -1,5 +1,11 @@
/** Public security runtime helpers for plugin-side trust boundaries. */
import {
replaceFileAtomic as replaceFsSafeFileAtomic,
type ReplaceFileAtomicFileSystem,
type ReplaceFileAtomicOptions,
} from "@openclaw/fs-safe/atomic";
export {
assertNoSymlinkParents,
assertNoSymlinkParentsSync,
@ -63,7 +69,19 @@ export {
} from "../infra/fs-safe.js";
export { sanitizeUntrustedFileName } from "@openclaw/fs-safe/advanced";
export { privateFileStoreSync } from "../infra/private-file-store.js";
export { movePathWithCopyFallback, replaceFileAtomic } from "@openclaw/fs-safe/atomic";
export { movePathWithCopyFallback } from "@openclaw/fs-safe/atomic";
// Keep the shipped, ignored adapter member source-compatible without wrapping the operation.
export const replaceFileAtomic: (
options: ReplaceFileAtomicOptions & {
fileSystem?: {
promises: ReplaceFileAtomicFileSystem["promises"] & {
/** @deprecated Omit this member; permissions use the retained FileHandle. */
chmod?: typeof import("node:fs/promises").chmod;
};
};
},
) => ReturnType<typeof replaceFsSafeFileAtomic> = replaceFsSafeFileAtomic;
export { ensurePortAvailable } from "../infra/ports.js";

View file

@ -9,11 +9,14 @@ export {
withTempDownloadPath,
} from "../infra/temp-download.js";
export {
tempWorkspace,
tempWorkspaceSync,
type TempWorkspace,
type TempWorkspaceOptions,
type TempWorkspaceSync,
withTempWorkspace,
withTempWorkspaceSync,
} from "@openclaw/fs-safe/temp";
export {
tempWorkspace,
withTempWorkspace,
type CompatibleTempWorkspace as TempWorkspace,
} from "../infra/fs-safe-compat.js";