mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-20 09:47:53 +00:00
refactor: centralize bounded file reads in fs-safe (#111104)
* refactor: use fs-safe bounded descriptor reads * build: update fs-safe to 0.4.2 * build: refresh root npm shrinkwrap * fix: satisfy bounded read return paths * fix: update fs-safe integration for latest main * fix: adopt fs-safe overflow compatibility release * build: complete fs-safe lockfile update * build: update fs-safe to 0.4.4 * build: refresh plugin SDK API baseline * test: follow fs-safe bounded read seam
This commit is contained in:
@@ -13,7 +13,7 @@ e5e67ddf3cab38fcbf9220bc3160715897e2709d9a9ff6ff36f1ecc9453c2367 module/agent-c
|
||||
30452bae2a689fb75dcb6dba9ef11e5b95f9cbe0c62eb190a0fcb82d0a19ae52 module/agent-core
|
||||
74daa746deb548379d3f0d6eac3c4d082df1034c4360cc03bf51fee0f10a2e4d module/agent-harness
|
||||
e09226afd443cee02ed648f032f53bfeb60585617bb523a649995764d3c38da9 module/agent-harness-exec-review-runtime
|
||||
71ac9e3d66263fc973f3ef724b70300d66d79bdf2f61c6e8478fb49790d81a29 module/agent-harness-runtime
|
||||
ca9810b66aff3c8b60278b80d634c9c280322034964c05cb727d84252080aabf module/agent-harness-runtime
|
||||
ec22d7a039fb58d0b8343ad149322960d3d8ca58b3f4c70f2fa8a099f8186d0c module/agent-harness-task-runtime
|
||||
5f63bf587bf3547d59d0dc5d0dc2fee54745aa6edaab4aa3ae700dba03443edb module/agent-harness-tool-runtime
|
||||
5168648cd946abad8a92822889f13ceacc87ed502314a66190d0b1eb8ebe76ea module/agent-media-payload
|
||||
@@ -35,13 +35,13 @@ a5e9215460bc99fb6e6cdd4bc67eb6466a5e99f28279e13d1769599c18dfe0b3 module/approva
|
||||
6c81b9122ab0a5c3190700c0a234047274a8d48edddcc8f26d3859b886696068 module/async-lock-runtime
|
||||
ad60ccc4fe9084d47f0477e02d9296bacad32f26d7456e2a84be8d25a53a25c2 module/boolean-param
|
||||
333a906d4ad9d3e89102ab7eb9059a7f9c7277328041223ee3b0713442f56a29 module/browser-config
|
||||
3a048c3733715f98e839f41a3a50c92e16d432e35bd255987424b7edd3711782 module/bundled-channel-config-schema
|
||||
0de3c53c3ba6ff739d4b047b440588ac25fdea3b795e92b37cab90bfd1520a8c module/bundled-channel-config-schema
|
||||
b6e53fb9c69840d71785325cd9a244809ffd6d3f0dd08ed0cccf816dd993f210 module/channel-actions
|
||||
fcfd76dbfe818e8e8574a425f340fee935acf53d4484e961f54c3ec760fb15fe module/channel-activity-runtime
|
||||
24c53c9cefacd8c1bec2aa91ba2b01e606f8b8477bc5cb2f99567225a4dcf67c module/channel-config-helpers
|
||||
a82cc04b1cda7a647850a13c3de552ab638550c7042f887f32bb0ced7227d143 module/channel-config-primitives
|
||||
929b3f55b60e3885e33644ed3e1757b82349e1beeeede02eda80f7bf7eea4631 module/channel-config-schema
|
||||
16e36f0ddc2ce83cd60e1a96cb4838887e7b328c912ccebbdc9a5d45c550bd07 module/channel-config-schema-legacy
|
||||
16dc9d32e8ca3ef78fc63e0fc4b20e6b943633e9572de1a49d24a880b6ffc66c module/channel-config-primitives
|
||||
5da2caccf30780a4cf86ac710e2ca42cc9576cb1c49291c0929889fec0cc2257 module/channel-config-schema
|
||||
080d51451ad15a2787eada3dcc04985154cc394058dfbf2194c6560801f3a1c1 module/channel-config-schema-legacy
|
||||
8740f7cc786a380043e41aaf710ac47931b3587c122bb152d5f0231f98fd5351 module/channel-config-writes
|
||||
2dd98659d9600e755f09ef00dd91c36692b8562ed3700461937e94f6cd1e640a module/channel-contract
|
||||
3db1a968e5b8aa97623a48a9ee76393d578d4bb90634fa59f499a2fd87b9da72 module/channel-core
|
||||
@@ -89,13 +89,13 @@ a224ce0381a32ed7a9d2261669c444f7f46701c15bf2f6121e9b057d9910da78 module/command
|
||||
eb4c757fe0086c1dbfa4c3f3caf3dcff0d3cab3924c608237f08f740a6ee5f59 module/command-status
|
||||
10f073c8c7c7384843cb89548e01cf66241e23ebabea01ccc1f9ac7e774f563e module/command-status-runtime
|
||||
d340686cf814326b5a554a32cc5ca324a9e1254a933c4d9d8d1ada5ac7109a10 module/command-surface
|
||||
251197439b471bc18d7470023b75a4b342884d7096217de9c7e8831ad037818f module/compat
|
||||
e26e0f75b43c5bbadd34401b21d8c76406ca0b87cc997be5abd100462d318f1a module/compat
|
||||
2cf2f7732af5491a14466fc984f833ddce2b0f6a20b1c82796c3ee856e2966ce module/concurrency-runtime
|
||||
a821f9cc4e6f9339399d99e73f58c3b00baf139aa9d85c22d73c89d8be5702d2 module/config-contracts
|
||||
235a9e4d983042c3db156efcab0e333984cbbf13ebdfcc44a3a5ab40cf3edb4f module/config-contracts
|
||||
20f3f8042de53e4eee61b64de9102c8c202b9299e6a29235647a4729f70145f2 module/config-mutation
|
||||
316949815affe623ac63951a5db580527f02663576dffa084f02768f612c0c1c module/config-runtime
|
||||
0415d7b2b042d44990b3c1ffb7e648bd791bcded3128af35d1a02444f24bce91 module/config-schema
|
||||
566ad45c5702acbd7606e2473f5a00c0d4f21722fb9056243b43171cfc856e3e module/config-types
|
||||
adb5fa3476c1f6b4e6c6d2d170254ed781949e7b609be1679661003911635de3 module/config-schema
|
||||
1531347939d51c528b7230b951da194faafdfc3d8284765e7e9a0373d1ce5ef8 module/config-types
|
||||
42d15153981cfe3adc1d5f91621434c56f07a9bd48c15ce34742f72dd040c142 module/context-visibility-runtime
|
||||
03636897fb99cb73e4d8620c8a0e0d72b4d52fc32bf94f525af2aa88c489c6c2 module/conversation-binding-runtime
|
||||
c1ea9510dfda047609a99d5d2cd1f1560f5d469a36e6b695766213d695c25b0f module/conversation-runtime
|
||||
@@ -111,7 +111,7 @@ f70c93d28053ca2e8353e45e6515ce7acef188097c6117d1545965d0699c8004 module/device-
|
||||
371ee1fd78810526745c93c24c4b85562c981676adcc33aaa0c627f5d2d45810 module/direct-dm-guard-policy
|
||||
91278c800e0f87d7111f226e1cfcb6f29552936fec7215b3b69be4da7e2ee8fb module/directory-config-runtime
|
||||
ea81ef06956c1bc0853fa00afbbc2b5a4019116aaf8a436e1b27d06f7a2c9e88 module/directory-runtime
|
||||
eb069b02d837a4657f8230d0efaaf7cdc913a5ba4866d90f44620f1a8d06c3fe module/discord
|
||||
c443b68d232f28b7241e1be10d1604def0d52d99ce3ae1ca8e2be3c6fd8b2d2f module/discord
|
||||
f41f9b34ab771c894293453bcdf072860c7cd509dd4e0172156634a816b8d727 module/document-extractor
|
||||
3ac20ebba52de5a2f18807c0c8ade6e61f59e260a35fc1d077c9dedb9960dabd module/embedding-providers
|
||||
46c05a90b66032d1d7ad08445840a4bf81aa2bd325348f87710ad4538daf38f6 module/error-runtime
|
||||
@@ -225,7 +225,7 @@ c543747f32aebde42508e31606db325739f0c54fcc7cf683f68ac941c835f737 module/provide
|
||||
096f53f25cde2c3428b6ff042f1687c5a7c1b161a375ba97aefcdf2f1c38887f module/provider-setup
|
||||
413af0d4597c1bab45e2f1a260b520445a25b9d11abd46a98fdfb74c60ddd601 module/provider-stream
|
||||
ea8bd63655e983dacfd306fe77d0793097166228a2dabd9b7fc8e33252eb359f module/provider-stream-family
|
||||
dc720974ed8d5cfd98fb3bb2958630736883f38317ee4744ce0b4042fa4485d8 module/provider-stream-shared
|
||||
2285e2c23d64af94ff6b9bd6108e854343f1ec55eca6212fc4fd22138253363b module/provider-stream-shared
|
||||
5a907292055c941ec7420163ea50e8bc90ead816b3c5867d94a23024d3b579e8 module/provider-tools
|
||||
8236935f56423a26aeb7219173c0c8c8c7aacb4a13d0d851f05bdf2d35790cd4 module/provider-transport-runtime
|
||||
009a594b19aa9f0d0c0c28f54968f807cad4b3ee40ec13c685367116d63aafdc module/provider-usage
|
||||
@@ -272,7 +272,7 @@ e9c4398b04d04fead0e46ec618589d8f597a0b6087805ab132138e941b76e190 module/sandbox
|
||||
596a315d426121c9620b314e3a9a7f523840b46e007d94d0d5e83cdedf789d15 module/security-runtime
|
||||
50beebb77e461deaccdbff038f6a461dff1d5773426322dc6d7991f6e05a7c37 module/self-hosted-provider-setup
|
||||
250476d121ffa4ed67d497c59d9c7bb1886973e92ebed39325a02609217bade0 module/session-binding-runtime
|
||||
2866ff45859edc6a299cd36eaaf0b9ef32181cc6f0370b7e71eb9deddea5989c module/session-catalog
|
||||
e3cfdf7ff5181ecd517bcb965c11f78b8363c8adf9f7f53f18bdbb5e0ceca1a6 module/session-catalog
|
||||
f07839f5b8929a179857a0b4b89f8448864078cd5972dd53f9a30e50bf55408d module/session-key-runtime
|
||||
f59099aa2d536246d4b1297f01bcea66796351a9259debdd45909afeb7a42d86 module/session-store-runtime
|
||||
b1d0a76337122cb9dbb0c89fbb8bfacc1a88ce689270d3d684019c9a03ce669a module/session-transcript-hit
|
||||
@@ -299,7 +299,7 @@ eb9a25321eaa2bbea1721f7d4b8b218bed398379494e09a21a621f264435b39a module/string-
|
||||
32f031eb75c887b24b8eaa693cd0aa1a4648dca85eab7bfdfb3d08136861c274 module/system-event-runtime
|
||||
65361dc9a23578787c1ccf98f7df99e443574614e7ce95889f9a295725e12fae module/talk-config-runtime
|
||||
9efd666b8c2cc8a9abf816751fd9420f3c048d158e48570cae8b7a4cbc52a83f module/target-resolver-runtime
|
||||
cb77f0dcdd4f360cbc5efbb657f57f8ed5840e66adc903a23385ba6d42ca0101 module/telegram-account
|
||||
da05dc1506e226fc25285a2963bd8d96dd0e882d6baa24b8073a0c53fef9fc81 module/telegram-account
|
||||
7729f9f201c08f114925da75ee86a5a8deb6677e5d1b5ee86bc60aacaa01f63b module/telegram-command-config
|
||||
110944726884fca94f38c9c329b5950629438b9a719f4782c4beeade8bd67746 module/temp-path
|
||||
a65f17db3d04c2ca1b34f9a4ebe8748952bbcb00318b741adbe3f227d2e2de20 module/text-autolink-runtime
|
||||
|
||||
Generated
+5
-5
@@ -23,7 +23,7 @@
|
||||
"@mistralai/mistralai": "2.4.0",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@mozilla/readability": "0.6.0",
|
||||
"@openclaw/fs-safe": "0.4.1",
|
||||
"@openclaw/fs-safe": "0.4.4",
|
||||
"@openclaw/proxyline": "0.3.3",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"chalk": "5.6.2",
|
||||
@@ -434,16 +434,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@openclaw/fs-safe": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.4.1.tgz",
|
||||
"integrity": "sha512-hQi+BxO10KdRFlYUot1syC+hTaUnGeQNdqX5kwkKJig8CFq1tKsYJLPm+zkiiGsSKOprPAquQl/txejEhpKPgg==",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.4.4.tgz",
|
||||
"integrity": "sha512-QklFGshaQDXl7RFyxPeSN/1moYq/X+SJcmX/nY3oXJO/tzVasL9i1g3m4nJqIfd6qUTJQjeu+41aIqw8+SbUww==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"jszip": "^3.10.1",
|
||||
"tar": "7.5.19"
|
||||
"tar": "7.5.20"
|
||||
}
|
||||
},
|
||||
"node_modules/@openclaw/proxyline": {
|
||||
|
||||
+1
-1
@@ -2055,7 +2055,7 @@
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@mozilla/readability": "0.6.0",
|
||||
"@openclaw/ai": "workspace:*",
|
||||
"@openclaw/fs-safe": "0.4.1",
|
||||
"@openclaw/fs-safe": "0.4.4",
|
||||
"@openclaw/proxyline": "0.3.3",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"chalk": "5.6.2",
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
diff --git a/dist/pinned-python.js b/dist/pinned-python.js
|
||||
index c6177eaf56295780ef2edba47ca8bee02cdcdbf2..f6f361974d48f8f4abd3f1e47f1fdb10bc3f2f77 100644
|
||||
--- a/dist/pinned-python.js
|
||||
+++ b/dist/pinned-python.js
|
||||
@@ -93,6 +93,12 @@ def write_all(fd, data):
|
||||
if written <= 0:
|
||||
raise OSError(errno.EIO, "short write")
|
||||
view = view[written:]
|
||||
+def fsync_best_effort(fd):
|
||||
+ try:
|
||||
+ os.fsync(fd)
|
||||
+ except OSError as error:
|
||||
+ if error.errno != errno.EPERM:
|
||||
+ raise
|
||||
def link_unsupported(exc):
|
||||
unsupported = (errno.EPERM, errno.EOPNOTSUPP, getattr(errno, "ENOTSUP", errno.EOPNOTSUPP))
|
||||
return getattr(exc, "errno", None) in unsupported
|
||||
@@ -298,13 +304,13 @@ def write_path(root_fd, payload):
|
||||
temp_name, temp_fd = create_temp_file(parent_fd, basename, mode)
|
||||
os.fchmod(temp_fd, mode)
|
||||
write_all(temp_fd, data)
|
||||
- os.fsync(temp_fd)
|
||||
+ fsync_best_effort(temp_fd)
|
||||
temp_stat = os.fstat(temp_fd)
|
||||
os.close(temp_fd)
|
||||
temp_fd = None
|
||||
result_stat = commit_temp_file(parent_fd, temp_name, basename, overwrite, mode, temp_stat)
|
||||
temp_name = None
|
||||
- os.fsync(parent_fd)
|
||||
+ fsync_best_effort(parent_fd)
|
||||
return {"dev": result_stat.st_dev, "ino": result_stat.st_ino}
|
||||
finally:
|
||||
if temp_fd is not None:
|
||||
diff --git a/dist/pinned-write.js b/dist/pinned-write.js
|
||||
index 77d0ddfa5a1df2de0449c806d19781daaed4910b..09dd77aef95659865b93ce29e15f26e874dd0c25 100644
|
||||
--- a/dist/pinned-write.js
|
||||
+++ b/dist/pinned-write.js
|
||||
@@ -31,6 +31,16 @@ function assertWithinMaxBytes(bytes, maxBytes) {
|
||||
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
||||
}
|
||||
}
|
||||
+async function syncFileBestEffort(handle) {
|
||||
+ try {
|
||||
+ await handle.sync();
|
||||
+ }
|
||||
+ catch (error) {
|
||||
+ if (error?.code !== "EPERM") {
|
||||
+ throw error;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
async function writeStreamToHandle(stream, handle, maxBytes) {
|
||||
let bytes = 0;
|
||||
for await (const chunk of stream) {
|
||||
@@ -192,7 +202,7 @@ async function runPinnedWriteFallback(params) {
|
||||
else {
|
||||
await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
|
||||
}
|
||||
- await handle.sync();
|
||||
+ await syncFileBestEffort(handle);
|
||||
const stat = await handle.stat();
|
||||
await handle.close().catch(() => undefined);
|
||||
await syncDirectoryBestEffort(parentPath);
|
||||
@@ -237,7 +247,7 @@ async function runPinnedWriteFallback(params) {
|
||||
throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
|
||||
}
|
||||
const expectedTempStat = tempStat;
|
||||
- await handle.sync();
|
||||
+ await syncFileBestEffort(handle);
|
||||
await handle.close().catch(() => undefined);
|
||||
handle = undefined;
|
||||
await withAsyncDirectoryGuards([parentGuard], async () => {
|
||||
Generated
+5
-8
@@ -36,9 +36,6 @@ overrides:
|
||||
|
||||
packageExtensionsChecksum: sha256-zZ8fyodhMTumshonC7kktCqTPsiHL3UAyS9vltFAlMo=
|
||||
|
||||
patchedDependencies:
|
||||
'@openclaw/fs-safe@0.4.1': e49004277fcb3e714125baf15a996682c7310e8e9269c865cc5bcb9fef46a57c
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -86,8 +83,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:packages/ai
|
||||
'@openclaw/fs-safe':
|
||||
specifier: 0.4.1
|
||||
version: 0.4.1(patch_hash=e49004277fcb3e714125baf15a996682c7310e8e9269c865cc5bcb9fef46a57c)
|
||||
specifier: 0.4.4
|
||||
version: 0.4.4
|
||||
'@openclaw/proxyline':
|
||||
specifier: 0.3.3
|
||||
version: 0.3.3(undici@8.5.0)
|
||||
@@ -3702,8 +3699,8 @@ packages:
|
||||
engines: {node: '>=22'}
|
||||
hasBin: true
|
||||
|
||||
'@openclaw/fs-safe@0.4.1':
|
||||
resolution: {integrity: sha512-hQi+BxO10KdRFlYUot1syC+hTaUnGeQNdqX5kwkKJig8CFq1tKsYJLPm+zkiiGsSKOprPAquQl/txejEhpKPgg==}
|
||||
'@openclaw/fs-safe@0.4.4':
|
||||
resolution: {integrity: sha512-QklFGshaQDXl7RFyxPeSN/1moYq/X+SJcmX/nY3oXJO/tzVasL9i1g3m4nJqIfd6qUTJQjeu+41aIqw8+SbUww==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@openclaw/libterminal@0.3.2':
|
||||
@@ -10072,7 +10069,7 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@openclaw/fs-safe@0.4.1(patch_hash=e49004277fcb3e714125baf15a996682c7310e8e9269c865cc5bcb9fef46a57c)':
|
||||
'@openclaw/fs-safe@0.4.4':
|
||||
optionalDependencies:
|
||||
jszip: 3.10.1
|
||||
tar: 7.5.19
|
||||
|
||||
+1
-2
@@ -9,7 +9,7 @@ minimumReleaseAge: 2880
|
||||
|
||||
minimumReleaseAgeExclude:
|
||||
- "@openclaw/crabline@0.1.11"
|
||||
- "@openclaw/fs-safe@0.4.1"
|
||||
- "@openclaw/fs-safe@0.4.4"
|
||||
- "@openclaw/libterminal@0.3.2"
|
||||
- "@openclaw/proxyline@0.3.3"
|
||||
- "@openclaw/uirouter@0.1.0"
|
||||
@@ -155,4 +155,3 @@ packageExtensions:
|
||||
optional: true
|
||||
|
||||
patchedDependencies:
|
||||
"@openclaw/fs-safe@0.4.1": patches/@openclaw__fs-safe@0.4.1.patch
|
||||
|
||||
@@ -8,8 +8,6 @@ import { fileURLToPath } from "node:url";
|
||||
import YAML from "yaml";
|
||||
|
||||
const ALLOWED_PATCHED_DEPENDENCIES = new Map([
|
||||
// Remove after fs-safe ships pinned-write fsync with best-effort EPERM handling.
|
||||
["@openclaw/fs-safe@0.4.1", "patches/@openclaw__fs-safe@0.4.1.patch"],
|
||||
["baileys@7.0.0-rc12", "patches/baileys@7.0.0-rc12.patch"],
|
||||
["baileys@7.0.0-rc13", "patches/baileys@7.0.0-rc13.patch"],
|
||||
]);
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "node:path";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { openRootFileSync } from "../infra/boundary-file-read.js";
|
||||
import { readFileDescriptorBoundedSync } from "../infra/file-descriptor-read.js";
|
||||
import { readFileDescriptorBoundedSync } from "../infra/boundary-file-read.js";
|
||||
import { isRenderableAvatarImageDataUrl } from "../shared/avatar-limits.js";
|
||||
import {
|
||||
AVATAR_MAX_BYTES,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileDescriptorBounded } from "../infra/file-descriptor-read.js";
|
||||
import { readFileDescriptorBounded } from "../infra/boundary-file-read.js";
|
||||
|
||||
// Workspace bootstrap files are model context, not arbitrary attachments. Keep every
|
||||
// read path on the same bound so compaction and sandbox copies cannot bypass it.
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
normalizeStringifiedOptionalString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import JSON5 from "json5";
|
||||
import { readFileDescriptorBoundedSync } from "../infra/boundary-file-read.js";
|
||||
import { hasErrnoCode } from "../infra/errors.js";
|
||||
import { readFileDescriptorBoundedSync } from "../infra/file-descriptor-read.js";
|
||||
|
||||
export type ConfigSetOptions = {
|
||||
strictJson?: boolean;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { getTerminalTableWidth, renderTable } from "../../packages/terminal-core
|
||||
import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
|
||||
import { readBestEffortConfig, type OpenClawConfig } from "../config/config.js";
|
||||
import { ADMIN_SCOPE, APPROVALS_SCOPE, type OperatorScope } from "../gateway/method-scopes.js";
|
||||
import { readFileDescriptorBounded } from "../infra/boundary-file-read.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import {
|
||||
collectExecPolicyScopeSnapshots,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
type ExecApprovalsDefaults,
|
||||
type ExecApprovalsFile,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import { readFileDescriptorBounded } from "../infra/file-descriptor-read.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { callGatewayFromCli } from "./gateway-rpc.js";
|
||||
|
||||
@@ -64,7 +64,7 @@ async function readPlanFile(pathname: string): Promise<SecretsApplyPlan> {
|
||||
// Apply consumes a generated plan shape, not arbitrary JSON.
|
||||
const [fsModule, { readFileDescriptorBounded }, { isSecretsApplyPlan }] = await Promise.all([
|
||||
fsModuleLoader.load(),
|
||||
import("../infra/file-descriptor-read.js"),
|
||||
import("../infra/boundary-file-read.js"),
|
||||
import("../secrets/plan.js"),
|
||||
]);
|
||||
const fsConstants = fsModule.constants as typeof fsModule.constants & { O_NONBLOCK?: number };
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { isGatewaySecretRefUnavailableError } from "../gateway/credentials.js";
|
||||
import { ADMIN_SCOPE } from "../gateway/operator-scopes.js";
|
||||
import { createAbortError } from "../infra/abort-signal.js";
|
||||
import { readFileDescriptorBounded } from "../infra/file-descriptor-read.js";
|
||||
import { readFileDescriptorBounded } from "../infra/boundary-file-read.js";
|
||||
import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js";
|
||||
import { routeLogsToStderr } from "../logging/console.js";
|
||||
import {
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
} from "../agents/identity-avatar.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { matchRootFileOpenFailure, openRootFileSync } from "../infra/boundary-file-read.js";
|
||||
import { readFileDescriptorBounded } from "../infra/boundary-file-read.js";
|
||||
import {
|
||||
isPackageProvenControlUiRootSync,
|
||||
resolveControlUiRootSync,
|
||||
} from "../infra/control-ui-assets.js";
|
||||
import { resolveDevInstallGitBranch } from "../infra/dev-install-branch.js";
|
||||
import { listDevicePairing, verifyDeviceToken } from "../infra/device-pairing.js";
|
||||
import { readFileDescriptorBounded } from "../infra/file-descriptor-read.js";
|
||||
import { openLocalFileSafely, FsSafeError } from "../infra/fs-safe.js";
|
||||
import { safeFileURLToPath } from "../infra/local-file-access.js";
|
||||
import { verifyPairingToken } from "../infra/pairing-token.js";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { normalizeTrimmedStringList } from "@openclaw/normalization-core/string-
|
||||
import { MANIFEST_KEY } from "../compat/legacy-names.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { openRootFileSync } from "../infra/boundary-file-read.js";
|
||||
import { readFileDescriptorBoundedSync } from "../infra/file-descriptor-read.js";
|
||||
import { readFileDescriptorBoundedSync } from "../infra/boundary-file-read.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { isPathInsideWithRealpath } from "../security/scan-paths.js";
|
||||
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// Tests safe boundary file reads against upstream fs-safe behavior.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import * as upstream from "@openclaw/fs-safe/advanced";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import * as shim from "./boundary-file-read.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
describe("root file open shim", () => {
|
||||
it("re-exports the fs-safe root file helpers", () => {
|
||||
expect(shim.canUseRootFileOpen).toBe(upstream.canUseRootFileOpen);
|
||||
@@ -10,4 +15,28 @@ describe("root file open shim", () => {
|
||||
expect(shim.openRootFile).toBe(upstream.openRootFile);
|
||||
expect(shim.openRootFileSync).toBe(upstream.openRootFileSync);
|
||||
});
|
||||
|
||||
it("preserves the existing overflow error for fs-safe descriptor reads", async () => {
|
||||
const dir = tempDirs.make("openclaw-boundary-file-read-");
|
||||
const filePath = path.join(dir, "oversized.txt");
|
||||
fs.writeFileSync(filePath, "oversized");
|
||||
|
||||
const asyncFd = fs.openSync(filePath, "r");
|
||||
try {
|
||||
await expect(shim.readFileDescriptorBounded(asyncFd, 4)).rejects.toThrow(
|
||||
new RangeError("File exceeds 4 bytes"),
|
||||
);
|
||||
} finally {
|
||||
fs.closeSync(asyncFd);
|
||||
}
|
||||
|
||||
const syncFd = fs.openSync(filePath, "r");
|
||||
try {
|
||||
expect(() => shim.readFileDescriptorBoundedSync(syncFd, 4)).toThrow(
|
||||
new RangeError("File exceeds 4 bytes"),
|
||||
);
|
||||
} finally {
|
||||
fs.closeSync(syncFd);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// Exposes root-scoped file open helpers with fs-safe defaults.
|
||||
import "./fs-safe-defaults.js";
|
||||
import {
|
||||
readFileDescriptorBounded as readFileDescriptorBoundedFsSafe,
|
||||
readFileDescriptorBoundedSync as readFileDescriptorBoundedSyncFsSafe,
|
||||
} from "@openclaw/fs-safe/advanced";
|
||||
import { FsSafeError } from "@openclaw/fs-safe/errors";
|
||||
|
||||
// Root-scoped file open helpers. Use these for user paths that must stay under
|
||||
// an already trusted boundary.
|
||||
@@ -11,3 +16,28 @@ export {
|
||||
type RootFileOpenFailure,
|
||||
type RootFileOpenResult,
|
||||
} from "@openclaw/fs-safe/advanced";
|
||||
|
||||
function preserveOpenClawOverflowError(error: unknown, maxBytes: number): never {
|
||||
if (error instanceof FsSafeError && error.code === "too-large") {
|
||||
throw new RangeError(`File exceeds ${maxBytes} bytes`, { cause: error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
/** Read a pinned descriptor without changing OpenClaw's user-facing overflow error. */
|
||||
export async function readFileDescriptorBounded(fd: number, maxBytes: number): Promise<Buffer> {
|
||||
try {
|
||||
return await readFileDescriptorBoundedFsSafe(fd, maxBytes);
|
||||
} catch (error) {
|
||||
return preserveOpenClawOverflowError(error, maxBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/** Synchronous variant for callers that own a pinned descriptor. */
|
||||
export function readFileDescriptorBoundedSync(fd: number, maxBytes: number): Buffer {
|
||||
try {
|
||||
return readFileDescriptorBoundedSyncFsSafe(fd, maxBytes);
|
||||
} catch (error) {
|
||||
return preserveOpenClawOverflowError(error, maxBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// Bounded reads for file descriptors already pinned by a boundary open.
|
||||
import fs from "node:fs";
|
||||
|
||||
const READ_CHUNK_BYTES = 64 * 1024;
|
||||
|
||||
function createScratchBuffer(maxBytes: number): Buffer {
|
||||
return Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, Math.max(1, maxBytes + 1)));
|
||||
}
|
||||
|
||||
function appendChunk(params: {
|
||||
chunks: Buffer[];
|
||||
scratch: Buffer;
|
||||
bytesRead: number;
|
||||
total: number;
|
||||
maxBytes: number;
|
||||
}): number {
|
||||
const total = params.total + params.bytesRead;
|
||||
if (total > params.maxBytes) {
|
||||
throw new RangeError(`File exceeds ${params.maxBytes} bytes`);
|
||||
}
|
||||
params.chunks.push(Buffer.from(params.scratch.subarray(0, params.bytesRead)));
|
||||
return total;
|
||||
}
|
||||
|
||||
/** Read at most maxBytes from the descriptor without an unbounded allocation. */
|
||||
export function readFileDescriptorBoundedSync(fd: number, maxBytes: number): Buffer {
|
||||
const chunks: Buffer[] = [];
|
||||
const scratch = createScratchBuffer(maxBytes);
|
||||
let total = 0;
|
||||
while (true) {
|
||||
const bytesRead = fs.readSync(fd, scratch, 0, scratch.length, null);
|
||||
if (bytesRead === 0) {
|
||||
return Buffer.concat(chunks, total);
|
||||
}
|
||||
total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
|
||||
}
|
||||
}
|
||||
|
||||
function readChunk(fd: number, scratch: Buffer): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.read(fd, scratch, 0, scratch.length, null, (error, bytesRead) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(bytesRead);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Async variant for request paths; caller retains descriptor ownership. */
|
||||
export async function readFileDescriptorBounded(fd: number, maxBytes: number): Promise<Buffer> {
|
||||
const chunks: Buffer[] = [];
|
||||
const scratch = createScratchBuffer(maxBytes);
|
||||
let total = 0;
|
||||
while (true) {
|
||||
const bytesRead = await readChunk(fd, scratch);
|
||||
if (bytesRead === 0) {
|
||||
return Buffer.concat(chunks, total);
|
||||
}
|
||||
total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
|
||||
}
|
||||
}
|
||||
@@ -656,12 +656,12 @@ describe("secret ref resolver", () => {
|
||||
|
||||
const sampleHandle = await fs.open(filePath, "r");
|
||||
const fileHandlePrototype = Object.getPrototypeOf(sampleHandle) as {
|
||||
readFile: typeof sampleHandle.readFile;
|
||||
read: typeof sampleHandle.read;
|
||||
};
|
||||
await sampleHandle.close();
|
||||
const readFileSpy = vi
|
||||
.spyOn(fileHandlePrototype, "readFile")
|
||||
.mockImplementation(() => new Promise<Buffer>(() => {}) as never);
|
||||
const readSpy = vi
|
||||
.spyOn(fileHandlePrototype, "read")
|
||||
.mockImplementation(() => new Promise(() => {}) as never);
|
||||
|
||||
try {
|
||||
await expect(
|
||||
@@ -681,7 +681,7 @@ describe("secret ref resolver", () => {
|
||||
),
|
||||
).rejects.toThrow('File provider "filemain" timed out');
|
||||
} finally {
|
||||
readFileSpy.mockRestore();
|
||||
readSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ describe("check-package-patches", () => {
|
||||
`packages:
|
||||
- .
|
||||
patchedDependencies:
|
||||
"@openclaw/fs-safe@0.4.1": "patches/@openclaw__fs-safe@0.4.1.patch"
|
||||
"baileys@7.0.0-rc12": "patches/baileys@7.0.0-rc12.patch"
|
||||
`,
|
||||
"utf8",
|
||||
@@ -69,13 +68,11 @@ patchedDependencies:
|
||||
path.join(dir, "pnpm-lock.yaml"),
|
||||
`lockfileVersion: '9.0'
|
||||
patchedDependencies:
|
||||
"@openclaw/fs-safe@0.4.1": fs-safe-hash
|
||||
baileys@7.0.0-rc12: a9aea1790d2c65b1ae543c77faca4119bbfb91ee3b6ca6c38d1cad4f5702ada2
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(path.join(dir, "patches", "baileys@7.0.0-rc12.patch"), "diff\n", "utf8");
|
||||
writeFileSync(path.join(dir, "patches", "@openclaw__fs-safe@0.4.1.patch"), "diff\n", "utf8");
|
||||
git(dir, ["add", "pnpm-workspace.yaml", "pnpm-lock.yaml", "patches"]);
|
||||
|
||||
expect(collectPackagePatchViolations(dir)).toEqual([]);
|
||||
|
||||
Reference in New Issue
Block a user