fix(gateway): safely deliver local file URLs from image tags (QQBot Windows) - #43332
fix(gateway): safely deliver local file URLs from image tags (QQBot Windows)#43332k176060444-lgtm wants to merge 2 commits into
Conversation
Verification ReviewSolid implementation. A few observations:
LGTM overall — well-tested with 15+ test cases covering Windows/POSIX/percent-encoded/UNC/code-block/inline-code scenarios. |
|
Verification Review — reviewed full diff, no issues found. The
Clean implementation with strong defense-in-depth. |
|
Thanks for the careful Windows Problems
Suggested changes
Automated hermes-sweeper review. |
|
@teknium1 — Addressed both concerns from your review. Diff contained to 3 files (+158/−45). Item 1: post-stream bare-path contract
Item 2: background-task file:// QQBot routing Validation:
Commit: The changes were kept limited to the reviewed concerns and the PR scope was not expanded. |
fcf3cbd to
26472cd
Compare
|
@teknium1 — Rebased PR #43332 on current main. The previous branch had CRLF churn (~21000 lines of noise); this clean rebuild contains only functional changes (+678/−54) and is now mergeable. Item 1: post-stream bare-path contract — Restored current-main Item 2: background-task file:// routing — Validation:
Commit: The changes were kept limited to the reviewed concerns and the PR scope was not expanded. |
Live QQBot Windows E2E validationValidated the current PR commits on a real QQBot Windows deployment after Environment:
Results:
For the image cases, the live QQBot logs confirmed local-image upload through This validates both review fixes in the real target environment:
|
Follow-up from live
|
5675ebc to
43593d6
Compare
2a112e4 to
6412d65
Compare
… main Transplant PR NousResearch#43332 net behavior onto latest upstream/main: - Add _normalize_file_url() to parse/validate file:// URIs (POSIX, Windows drive-letter, percent-encoding; reject UNC/drive-relative) - Extend extract_images() to accept file:// markdown/HTML image tags with mask-based false-positive prevention; keep upstream's validate_media_delivery_path / container->host translation chain - Post-stream delivery stays EXPLICIT-ONLY (NousResearch#20834): MEDIA: and file:// tags deliver; bare local paths are never auto-uploaded - Same-response dedup via namespace-tuple keys (MEDIA + file:// + HTTP exact URL); cross-response explicit resend still delivers - Keep upstream thread_metadata propagation in _deliver_media_from_response - Batch background image delivery via send_multiple_images (file:// aware) - Tests: keep upstream queued-delivery suite intact, add PR's file:// regression coverage (POSIX/Windows/unicode/space paths, dedup, bare-path rejection, resend)
_normalize_file_url() rejected the URI form produced by the gateway delivery batching code, which wraps local paths with the default urllib.parse.quote() (safe='/'). On Windows that percent-encodes the drive letter and backslashes into the authority segment (file://C%3A%5Cdir%5Ca.png), which urlparse then treats as a bogus UNC host, so local images silently fell back to send_image() with the raw file:// URI and were never delivered. Decode the authority segment and accept it only when it resolves to an absolute drive path (C:/...). UNC hosts, encoded UNC authorities, drive-relative paths and all other non-drive authorities stay rejected. Re-check the drive-letter form after percent-decoding so the encoded three-slash variant (file:///C%3A/dir/a.png) normalizes identically to the plain form. Also update the post-stream docstring to describe the actual explicit attachment contract (MEDIA: + file:// + http(s) image tags) and drop a dead unquote import from send_multiple_images. Adds regression coverage using the production default quote encoding: - _normalize_file_url accepts file://C%3A%5C... and decodes to C:/... - send_multiple_images routes it to send_image_file (never send_image)
30e8a7a to
2aaf21f
Compare
Summary
Safely deliver local
file://image URLs from explicit markdown/HTML image tags on QQBot (Windows) and other platforms, rebuilt cleanly on currentupstream/mainduring conflict resolution on 2026-08-12.Current head SHA:
2aaf21f62da1bd3f4cd50aac0627ee2ba30c06c0Behavior Contract
#20834): bare local paths in an already-streamed reply are treated as text the user has seen, never auto-promoted to attachments;extract_local_files()is NOT called in the post-stream path.MEDIA:/file://may be re-sent in later turns (#73771): cross-turn history dedup was removed on main; aMEDIA:directive or explicitfile://image tag in a later turn is the model deliberately attaching a file (including a user-requested resend) and must deliver again.MEDIA:/a.pngandfile:///C:/a.pngreferencing the same file deliver once per response;foo.pngvsfoo.png.backup.png(different files) both deliver; HTTP(S) URLs use exact-string comparison.file://delivery routes throughsend_multiple_images()→send_image_file()(decoded local path), never passed as a literal pathname to the HTTP-onlysend_image().urllib.parse.quote()(safe=/), which on Windows percent-encodes the drive letter and backslashes into the authority segment (e.g.file://C%3A%5Cpath%5Cimage.png)._normalize_file_url()decodes such URIs back to the local drive path and they still pass through the existingvalidate_media_delivery_path()security chain (container→host translation, symlink resolve, denylist/credential protection, strict mode).file://URIs are refused by_normalize_file_url().Changes (4 files, +1157/-56)
gateway/platforms/base.py(+188/-36)_normalize_file_url()to parse and validatefile://URIs (case-insensitive scheme, Windows two/three-slash variants, production default-quote()encoded-drive authority form, rejects UNC / non-local authority / drive-relative).extract_images()to supportfile://markdown/HTML image tags with mask-based false-positive prevention (fences/blockquotes/JSON/data-src).send_multiple_images()routes localfile://URIs through_normalize_file_url()→send_image_file()(case-insensitive scheme;FILE://also works).gateway/run.py(+59/-20)_deliver_media_from_response()capturesadapter.extract_images(cleaned)and merges explicitfile://image tags withMEDIA:paths into onesend_multiple_images()call.("local", normcase(path))/("url", url)).history_media_paths/_history_local_keysremoved — follows current main#73771semantics).send_multiple_images()with_image_dedup_keyspreventing MEDIA:path + file:// tag double-send within one response.tests/gateway/test_background_command.py(+210/-0)file://URIs route throughsend_multiple_images→send_image_file; uppercase-scheme variant.MEDIA:path+file://tag for the same file sends once per response.three_slash_windowsvariant skipped on non-Win32.tests/gateway/test_tts_media_routing.py(+700/-0)C:\...,/tmp/...,smb://); JSON-embedded anddata-srcnon-detection; UNC / non-local authority / drive-relative rejection; Windows two/three-slash, encoded-drive (file://C%3A%5C...) and case-insensitive scheme; same-responseMEDIA:path+file://dedup; substring-safe dedup;send_multiple_imagesround-trip.test_post_stream_history_dedup_*) that relied on the removedhistory_media_pathscontract.test_post_stream_explicit_resend_allowed_in_later_turn,test_post_stream_file_url_resend_allowed_in_later_turn) proving a later turn repeating the sameMEDIA:/file://still delivers.quote()encoding:file://C%3A%5C...normalizes to the Windows drive path and routes tosend_image_file()(neversend_image()).test_post_stream_file_url_windows_path_deliveredcorrectly skipped on non-Win32; resend tests run on all platforms.Test Results
pytest tests/gateway/test_tts_media_routing.py(Windows)pytest tests/gateway/test_background_command.py(Windows)upstream/mainbaseline (Windows)ruff check(all 4 changed files)git diff --checkThe 17 broad-suite failures are pre-existing Windows platform-path assertion issues (also failing identically on clean
main); this PR adds no new failures and passes 29 more tests than baseline. No CI checks are currently reported for this PR head; the repository's required aggregated status check therefore remains unsatisfied.Verification
py_compile: 4 files, all OKmergeable: MERGEABLE; PR is OPEN and not a draft