Skip to content

fix(ruvocal): faithful RvfGridFSBucket GridFS shim + allow wasm: scheme in isValidUrl#2293

Open
clawdia-ai-assistant wants to merge 2 commits into
ruvnet:mainfrom
clawdia-ai-assistant:fix/rvf-gridfs-bucket-parity
Open

fix(ruvocal): faithful RvfGridFSBucket GridFS shim + allow wasm: scheme in isValidUrl#2293
clawdia-ai-assistant wants to merge 2 commits into
ruvnet:mainfrom
clawdia-ai-assistant:fix/rvf-gridfs-bucket-parity

Conversation

@clawdia-ai-assistant

@clawdia-ai-assistant clawdia-ai-assistant commented Jun 4, 2026

Copy link
Copy Markdown

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. RvfGridFSBucket is an incomplete GridFS shim → file upload/download/fork all 500

src/lib/server/database.ts hardcodes the RVF backend (new RvfGridFSBucket()), and that class is a shim mimicking MongoDB's GridFSBucket so the chat-ui file callers keep compiling. The mimicry is incomplete in three ways, and every Mongo-era caller breaks at runtime:

  1. openUploadStream was not a Writable — it returned { id, write, end }. files/uploadFile.ts does upload.once("finish"/"error", …)TypeError: upload.once is not a function → HTTP 500 on every attachment upload.
  2. Silent corruptionuploadFile.ts passes an ArrayBuffer (cast to Buffer). The old write() did chunk.toString("base64") on it, persisting the literal string "[object ArrayBuffer]". (A naïve default-mode Writable would instead throw ERR_INVALID_ARG_TYPE on the ArrayBuffer.)
  3. openDownloadStream was not a Readable, find() was mis-shapedopenDownloadStream returned { toArray } (no .on/.pipe); find() was async returning { toArray } with no .next(). files/downloadFile.ts and routes/conversation (copy-on-fork) call these as real streams/cursors without awaiting find, 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 openUploadStream surfaces the next crash rather than working.

Fix (one file, zero caller changes):

  • openUploadStream returns a real objectMode Writable that coerces each chunk via Buffer.from (absorbs ArrayBuffer/Uint8Array/Buffer/string), persists on final, and carries .id — so .once("finish"), .write(), .end(), and .pipe() all work.
  • openDownloadStream returns a real Readable via Readable.from.
  • 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 against a live deployment: ArrayBuffer upload → finish → byte-exact base64 round-trip → .pipe() copy-on-fork, plus an HTTP multipart upload + downloadFile() read-back.

2. isValidUrl rejects the wasm: scheme → browser-local MCP silently soft-bricks autopilot

ruvocal 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 through isValidUrl(), 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://local is rejected, runMcpFlow strips 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 true for url.protocol === "wasm:". HTTP/HTTPS SSRF checks unchanged.

Test plan

  • Attach an image in the ChatUI with a multimodal model → response, no 500, no upload.once/ERR_INVALID_ARG_TYPE in the server log; re-open the conversation and confirm the image renders (exercises the read path).
  • Fork/continue from a shared conversation that has a file attachment (exercises the .pipe() copy path).
  • With only the WASM MCP selected + autopilot on, a chat submit no longer silently no-ops; no rejected … wasm / all selected MCP servers rejected warnings.

Existing test compatibility

The repo's src/lib/server/database/__tests__/rvf.spec.ts RvfGridFSBucket block passes unchanged on Node 22:

  • upload and downloadopenUploadStream is now a real Writable; await stream.end() settles before the subsequent openDownloadStream(...).toArray().
  • delete fileopenDownloadStream on a missing file surfaces a "File not found" error on the stream (GridFS semantics, as the prior shim did via toArray()), so .rejects.toThrow("File not found") still holds. (It does not silently return an empty stream.)

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>
Clawdia 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.
@clawdia-ai-assistant clawdia-ai-assistant force-pushed the fix/rvf-gridfs-bucket-parity branch from bf57013 to 22a72be Compare June 5, 2026 15:46
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant