fix(ruvocal): faithful RvfGridFSBucket GridFS shim + allow wasm: scheme in isValidUrl#2293
Open
clawdia-ai-assistant wants to merge 2 commits into
Open
Conversation
YiannisDermitzakis
pushed a commit
to derio-net/frank
that referenced
this pull request
Jun 5, 2026
…470) Phase 4/4 of 2026-05-27--orch--ruflo-upload-fix. - frank-gotchas hot file + paperclip-ruflo.md: document the incomplete RvfGridFSBucket GridFS shim (upload/download/copy-on-fork parity fix), RVF-is-the-only-backend (DATABASE_URL ignored), and the per-model supportsTools/forceTools toggle MCP-load requirement. - File upstream PR ruvnet/ruflo#2293 (GridFS parity fix + the still-pending wasm:// urlSafety allow-line); link it from the gotchas. - Retroactively extend the ruflo building post (RVF Surprise, Part Two) and operating post (upload-500 troubleshooting + gotcha). - Set spec + plan Status to Deployed; tick all Phase 4 steps, complete phase. Co-authored-by: Clawdia <clawdia-ai-assistant@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
added 2 commits
June 5, 2026 15:46
…d/download/fork)
RvfGridFSBucket mimics MongoDB's GridFSBucket so the chat-ui file callers
keep compiling against the RVF backend, but the mimicry is incomplete in
three ways and every Mongo-era caller breaks at runtime:
1. openUploadStream returned a plain { id, write, end } object, not a
Writable. uploadFile.ts does `upload.once("finish"/"error", ...)` ->
`TypeError: upload.once is not a function` -> HTTP 500 on every upload.
2. uploadFile.ts passes an ArrayBuffer (cast to Buffer). The old write()
did `chunk.toString("base64")` on it, persisting the literal string
"[object ArrayBuffer]" -- silent data corruption even when (1) didn't
crash first. A naive default-mode Writable would instead throw
ERR_INVALID_ARG_TYPE on the ArrayBuffer.
3. openDownloadStream returned { toArray } (no .on/.pipe) and find() was
async returning { toArray } with no .next(). downloadFile.ts and
conversation.ts (copy-on-fork) call these as real streams/cursors
without awaiting find -> reading a file back and forking a shared
conversation with attachments both 500 independently.
Fix, in this one file with zero caller changes:
- openUploadStream returns a real objectMode Writable that coerces the
chunk via Buffer.from (absorbs ArrayBuffer/Uint8Array/Buffer/string),
writes on `final`, and carries `.id` -- so `.once("finish")`,
`.write()`, `.end()`, and `.pipe()` all work.
- openDownloadStream returns a real Readable via Readable.from for an
existing file, and an error-stream ("File not found") for a missing one
-- matching GridFS semantics and the prior shim's missing-file behavior,
so the existing rvf.spec.ts "delete file" expectation still holds.
- find() is synchronous and returns a cursor exposing both next() and
toArray().
Storage stays base64 (download decodes, downloadFile re-encodes for its
{type:"base64"} return -- round-trip preserved). Verified end-to-end:
ArrayBuffer upload -> finish -> byte-exact base64 round-trip -> .pipe()
copy-on-fork, plus the existing rvf.spec.ts RvfGridFSBucket cases
(upload/download + delete-then-download-rejects) pass on Node 22.
…oads
ruvocal advertises an in-browser WebAssembly MCP server ("RVAgent Local")
to clients as `url: "wasm://local"`. The chat-ui SPA selects it by default
when no other MCP server is configured. On every chat submit the server
passes each selected MCP URL through isValidUrl(), which only permits
http/https against safe hostnames -- sensible SSRF prevention for HTTP MCPs,
but categorically wrong for a scheme the server never fetches (the browser
hosts the WASM module). `wasm://local` is rejected, runMcpFlow strips it,
sees zero valid servers, and returns "not_applicable"; with autopilot ON
the client then silently refuses to fire the POST -- the user picks a model,
types, hits send, and nothing happens, with no 4xx/5xx and no toast.
Early-return true for `url.protocol === "wasm:"` -- browser-local schemes
have nothing the server needs to validate. HTTP/HTTPS SSRF checks are
unchanged.
bf57013 to
22a72be
Compare
clawdia-ai-assistant
pushed a commit
to derio-net/agent-images
that referenced
this pull request
Jun 5, 2026
…m rvf.spec.ts) The rvf-gridfs-parity patch made openDownloadStream return an empty Readable for a missing file. Upstream ruvnet/ruflo's own rvf.spec.ts "delete file" case asserts that downloading a deleted/absent file rejects with "File not found" — the empty-Readable would silently resolve to [] and turn that into a red CI on our upstream PR (ruvnet/ruflo#2293). Make openDownloadStream surface a missing file as an error on the stream (GridFS semantics, and what the prior shim did via toArray()). Real callers are unaffected: downloadFile.ts guards with find().next() and conversation.ts iterates find().toArray() first, so neither calls openDownloadStream on an absent id; both also attach .on("error"). Verified end-to-end in Node 22 (existing-file round-trip, .pipe() copy-on-fork, and missing-file rejection). - patches/rvf-gridfs-parity.patch: openDownloadStream errors on missing file - test/rvf-gridfs-contract.test.mjs: sync inline shim + add missing-file case; fix the run command in the header (node --test needs a glob, not a dir) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two independent fixes to the RVF-backed ChatUI (ruvocal), both currently carried as out-of-tree patches in our deployment. They're unrelated and a reviewer can take either on its own; happy to split into two PRs if you'd prefer.
1.
RvfGridFSBucketis an incomplete GridFS shim → file upload/download/fork all 500src/lib/server/database.tshardcodes the RVF backend (new RvfGridFSBucket()), and that class is a shim mimicking MongoDB'sGridFSBucketso the chat-ui file callers keep compiling. The mimicry is incomplete in three ways, and every Mongo-era caller breaks at runtime:openUploadStreamwas not aWritable— it returned{ id, write, end }.files/uploadFile.tsdoesupload.once("finish"/"error", …)→TypeError: upload.once is not a function→ HTTP 500 on every attachment upload.uploadFile.tspasses anArrayBuffer(cast toBuffer). The oldwrite()didchunk.toString("base64")on it, persisting the literal string"[object ArrayBuffer]". (A naïve default-modeWritablewould instead throwERR_INVALID_ARG_TYPEon theArrayBuffer.)openDownloadStreamwas not aReadable,find()was mis-shaped —openDownloadStreamreturned{ toArray }(no.on/.pipe);find()wasasyncreturning{ toArray }with no.next().files/downloadFile.tsandroutes/conversation(copy-on-fork) call these as real streams/cursors without awaitingfind, so reading a file back and forking a shared conversation with attachments each 500 independently.(2) and (3) were latent because those paths only run once files exist, and uploads never succeeded — so fixing only
openUploadStreamsurfaces the next crash rather than working.Fix (one file, zero caller changes):
openUploadStreamreturns a real objectModeWritablethat coerces each chunk viaBuffer.from(absorbsArrayBuffer/Uint8Array/Buffer/string), persists onfinal, and carries.id— so.once("finish"),.write(),.end(), and.pipe()all work.openDownloadStreamreturns a realReadableviaReadable.from.find()is synchronous and returns a cursor exposing bothnext()andtoArray().Storage stays base64 (download decodes →
downloadFilere-encodes for its{type:"base64"}return; round-trip preserved). Verified end-to-end against a live deployment:ArrayBufferupload →finish→ byte-exact base64 round-trip →.pipe()copy-on-fork, plus an HTTP multipart upload +downloadFile()read-back.2.
isValidUrlrejects thewasm:scheme → browser-local MCP silently soft-bricks autopilotruvocal advertises an in-browser WASM MCP server ("RVAgent Local") to clients as
url: "wasm://local", and the SPA selects it by default when no other MCP is configured. On every chat submit the server runs each selected MCP URL throughisValidUrl(), which only permits http/https against safe hostnames — correct SSRF prevention for HTTP MCPs, but wrong for a scheme the server never fetches (the browser hosts the module).wasm://localis rejected,runMcpFlowstrips it, sees zero valid servers, returns"not_applicable"; with autopilot ON the client then silently refuses to fire the POST — pick a model, type, hit send, nothing happens, no 4xx/5xx, no toast.Fix: early-return
trueforurl.protocol === "wasm:". HTTP/HTTPS SSRF checks unchanged.Test plan
upload.once/ERR_INVALID_ARG_TYPEin the server log; re-open the conversation and confirm the image renders (exercises the read path)..pipe()copy path).rejected … wasm/all selected MCP servers rejectedwarnings.Existing test compatibility
The repo's
src/lib/server/database/__tests__/rvf.spec.tsRvfGridFSBucketblock passes unchanged on Node 22:upload and download—openUploadStreamis now a realWritable;await stream.end()settles before the subsequentopenDownloadStream(...).toArray().delete file—openDownloadStreamon a missing file surfaces a"File not found"error on the stream (GridFS semantics, as the prior shim did viatoArray()), so.rejects.toThrow("File not found")still holds. (It does not silently return an empty stream.)