diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index a44d3be5a92..d8dad615504 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -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 diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 60c14a3fd76..d0160862fde 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -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": { diff --git a/package.json b/package.json index d4d57a30dfe..569a23f7da1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/patches/@openclaw__fs-safe@0.4.1.patch b/patches/@openclaw__fs-safe@0.4.1.patch deleted file mode 100644 index 18ef0e9afd9..00000000000 --- a/patches/@openclaw__fs-safe@0.4.1.patch +++ /dev/null @@ -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 () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88eae63bd85..28906008ff2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0bb5f8a31e9..50ccbd99547 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 diff --git a/scripts/check-package-patches.mjs b/scripts/check-package-patches.mjs index b0c8bc1f3d2..4b7efc18fd6 100644 --- a/scripts/check-package-patches.mjs +++ b/scripts/check-package-patches.mjs @@ -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"], ]); diff --git a/src/agents/identity-avatar-file.ts b/src/agents/identity-avatar-file.ts index 682bc32e8e7..aab4930a162 100644 --- a/src/agents/identity-avatar-file.ts +++ b/src/agents/identity-avatar-file.ts @@ -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, diff --git a/src/agents/workspace-bootstrap-read.ts b/src/agents/workspace-bootstrap-read.ts index 282566161d5..dc29b8b401c 100644 --- a/src/agents/workspace-bootstrap-read.ts +++ b/src/agents/workspace-bootstrap-read.ts @@ -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. diff --git a/src/cli/config-set-input.ts b/src/cli/config-set-input.ts index de4cd486370..b637bc65ebb 100644 --- a/src/cli/config-set-input.ts +++ b/src/cli/config-set-input.ts @@ -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; diff --git a/src/cli/exec-approvals-cli.ts b/src/cli/exec-approvals-cli.ts index 6197321d278..3930b34af0e 100644 --- a/src/cli/exec-approvals-cli.ts +++ b/src/cli/exec-approvals-cli.ts @@ -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"; diff --git a/src/cli/secrets-cli.ts b/src/cli/secrets-cli.ts index 5dd2be11a7b..6ee775fff86 100644 --- a/src/cli/secrets-cli.ts +++ b/src/cli/secrets-cli.ts @@ -64,7 +64,7 @@ async function readPlanFile(pathname: string): Promise { // 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 }; diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index b26a6b02a05..46948ef1b50 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -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 { diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts index 7a428370256..e245c605fef 100644 --- a/src/gateway/control-ui.ts +++ b/src/gateway/control-ui.ts @@ -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"; diff --git a/src/hooks/workspace.ts b/src/hooks/workspace.ts index 23c364a9315..8b3b4ce40db 100644 --- a/src/hooks/workspace.ts +++ b/src/hooks/workspace.ts @@ -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"; diff --git a/src/infra/boundary-file-read.test.ts b/src/infra/boundary-file-read.test.ts index 2f7ce55b4f4..913dc44848f 100644 --- a/src/infra/boundary-file-read.test.ts +++ b/src/infra/boundary-file-read.test.ts @@ -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); + } + }); }); diff --git a/src/infra/boundary-file-read.ts b/src/infra/boundary-file-read.ts index 9c34ce812c3..39336f5e3cc 100644 --- a/src/infra/boundary-file-read.ts +++ b/src/infra/boundary-file-read.ts @@ -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 { + 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); + } +} diff --git a/src/infra/file-descriptor-read.ts b/src/infra/file-descriptor-read.ts deleted file mode 100644 index d88320b30a6..00000000000 --- a/src/infra/file-descriptor-read.ts +++ /dev/null @@ -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 { - 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 { - 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 }); - } -} diff --git a/src/secrets/resolve.test.ts b/src/secrets/resolve.test.ts index d653fa3c086..cd6b9aeacfa 100644 --- a/src/secrets/resolve.test.ts +++ b/src/secrets/resolve.test.ts @@ -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(() => {}) 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(); } }); diff --git a/test/scripts/check-package-patches.test.ts b/test/scripts/check-package-patches.test.ts index 152d42f254a..ffba67c9ee9 100644 --- a/test/scripts/check-package-patches.test.ts +++ b/test/scripts/check-package-patches.test.ts @@ -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([]);