Repository navigation
Conversation
First Principles Review (Opus 5.5) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nothing blocks: the 413 on large files is replaced by a confirm-then-stream path the Goal names. One mechanism costs more than the Goal needs. Notes
What this change shipsInventory (10 items) — 7 justifiedIntent: let the owner download a dashboard file of any size, with a warning before a file too large to scan. This is a FIX.
[FIRST-PRINCIPLES-REVIEWED] 2492eff Description read[DESCRIPTION-READ] ec06347913e6baa3ffaac5bdaf51fa3eaa44a4829d2e3a964c41e7b8e5c9f625 That sha256 names the PR description this verdict read. Recompute it with |
Design Review (Opus 5.5) — ✅ PASSDesign-level review of Design-Verdict: PASS Nothing blocks: the size split is decided by the fstat of the one open descriptor, the owner and sensitive-path gates hold at every size, and unscanned delivery needs the owner's confirmation and is audited. Notes
[DESIGN-REVIEWED] 2492eff Description read[DESCRIPTION-READ] 7a12e66cb7b2a236b92384edac35b6dd6cfc3475d80dd3dbe8a13a0c9c9290d7 That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
GPT 6.1 Review — ✅ no blocking findingsGPT 6.1 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
UX Review (Opus 5.5) — ✅ PASS (a BLOCK with no Trigger and no Outcome does not block)UX-level review of UX-Verdict: PASS cannot evaluate: missing the new download-refusal notice on the file card, the media fallback and the sheet bar. The diff adds it to all three places, and no screenshot shows any of them. Evidence gaps
Notes
[UX-REVIEWED] 2492eff Description read[DESCRIPTION-READ] 34d168bc2901b2c4098db9e7bb53c5cad53e502f94f45028f715f2659644c2be That sha256 names everything this verdict read: the PR description, plus the 5 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
Opus 5.5 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
9972e2e to
e46baf5
Compare
GET /api/file-download refused any file over 50 MB because it read the whole file into memory for its credential scans. Files up to 50 MB keep that path unchanged: whole read, redact / binary / wide scans, content_redacted refusal, owner-grant release, audit. A larger file is delivered unscanned, by the maintainer's decision: the route is owner-only and the scan cannot hold a multi-GB file. Without confirm_unscanned=1 it answers 409 large_file_unscanned with size and limit and sends nothing; with it the file streams in bounded chunks from the same O_NOFOLLOW descriptor, cut at its fstat size, and SEL tags the delivery unscanned_large_file. The owner check, path validation and sensitive-path refusal apply at every size. The dashboard asks first: every Download asks "Too large to scan" on that 409, and only "Download anyway" retries with the flag, as a plain browser download so the file streams to disk.
e46baf5 to
2492eff
Compare
Problem / Motivation
Goal: Download a file of any size from the dashboard, with a clear warning before a file too large to scan.
Download (file tree right-click, the viewer's Download, the binary card) refused any file over 50 MB with "Download failed".
GET /api/file-downloadread the whole file into memory for its credential scans and answered 413 above the upload cap, so a screen recording, a dataset or a build artifact could not be retrieved from the dashboard at all.Why it matters
The owner's own large files on the gateway host were unreachable from a remote or tunnelled browser, which is exactly when Download is the only way to get them. Raising the number would not fix it: a whole-file read of a multi-GB file would exhaust gateway memory.
Not a goal
/api/file-raw,/api/file-streamor/api/outbox/{filename}./apps/file-explorer/api/read(a separate 4 MB read cap), not/api/file-download.What changed (motivation → approach → change)
Symptom: Download fails for any file over 50 MB. Cause: the route used the whole-read envelope (
_open_checked,max_bytes=_MAX_UPLOAD_BYTES) so its scans could see the whole buffer.Approach, per the maintainer's decision: scan small files exactly as before, and stream large files unscanned behind an explicit confirmation.
dashboard/file_api/transfer.pyapi_file_downloadopens the file once through the shared checked prefix (_open_checked_file: validation, sensitive-path filter,O_NOFOLLOWopen, fstat) and decides small or large from the fstat of that one descriptor, so no other file can be swapped in between the check and the send._DOWNLOAD_SCAN_MAX_BYTES(=_MAX_UPLOAD_BYTES, 50 MB): today's path. Bounded read on the same fd, then the sameredact/binary_content_is_flagged/wide_content_is_flaggedscans, thecontent_redactedrefusal, the owner-grant release and its audit, unchanged. A file that grows past the threshold between the fstat and the read is refused 409file_changed.confirm_unscanned=1it answers 409{"code": "large_file_unscanned", "size", "limit"}and sends no bytes. With it,_stream_unscanned_downloadstreams_STREAM_CHUNK_BYTESchunks from the same fd withContent-Length= the fstat size, never past it (a file that grows is cut there, one that shrinks drops the connection). Sameattachment+nosniffheaders. SEL logs the success witherror="unscanned_large_file"(and the unconfirmed ask withlarge_file_unconfirmed), so the audit shows the file was not scanned.validate_tool_args, path validation or the sensitive-path refusal: a.envor credential path is refused at any size.dashboard/file_api/pinned_io.py: the open-refusal mapping moves out of_open_checkedinto_open_refusal, so the download route and the whole-read envelope share one SEL vocabulary and response set instead of a copy.downloadFileToDisk(utils/fileReadUrl.ts) reads the 409 and asks throughconfirmUnscannedDownload;UnscannedDownloadConfirmHost, mounted once inApp.tsx, answers with the shareduseConfirmdialog (Modal focus trap; Escape, backdrop and Cancel all cancel). "Download anyway" starts a plain browser download of theconfirm_unscanned=1URL (downloadUrlinutils/download.ts), so the file streams to disk instead of being held as a blob. Sizes usefmtBytes. The binary/office card, media fallback and sheet "Download" links becomeFileDownloadLink, which keeps the<a href download>but routes a plain click through the same helper, so they ask too and show a refusal instead of saving the error JSON.docs/system-specs/modules/security.mdgains § Dashboard File Download, recording that files over the threshold are delivered unscanned by the maintainer's decision and why;learn-cron-dashboard.mdno longer lists file-download among the 50 MB in-memory readers.Backwards compatibility
Compatible: every file that downloads today (up to 50 MB) takes the same scanned path with the same answers; the only response that changes is the one for a file over 50 MB, which was 413 and is now 409
large_file_unscanned(or the file, withconfirm_unscanned=1).Removes nothing: no route, key or reader goes away. The one guarantee that narrows is deliberate and documented: a file over 50 MB that was refused before is now delivered without a content scan once the owner confirms.
Tests
test/test_file_download.pytest_a_large_file_asks_before_it_sends_a_byte: over the (patched) threshold, no flag → 409 withsizeandlimit, SELlarge_file_unconfirmed.test_a_confirmed_large_file_streams_whole_and_unscanned: with the flag the bytes arrive whole withContent-Length, attachment and nosniff; no scan runs; SELunscanned_large_file.test_a_large_file_is_never_read_whole: every read on the large path is one bounded chunk.test_a_growing_large_file_stops_at_its_fstat_size: bytes appended after the fstat are not sent.test_a_small_file_ignores_the_flag_and_is_still_scanned: a credential file under the threshold is refusedcontent_redactedeven with the flag.test_a_sensitive_path_over_the_threshold_is_refused_even_confirmedandtest_a_non_owner_is_refused_even_confirmed: 403 at any size.test_a_stalled_chunk_read_holds_no_default_worker_and_ends_the_streamandtest_a_busy_transfer_pool_ends_the_stream: each chunk read runs on the bounded transfer pool (mc-pathxfer) within_DOWNLOAD_CHUNK_STALL_SECS; a read that never returns, or a refused hop, is audited and drops the connection. Both fail with the oldasyncio.to_threadread.test_after_a_stall_the_handler_returns_and_the_close_is_deferred: after a stalled chunk read the handler returns at once, never awaits the close on the pool, and the close runs on its ownfile-download-closethread (the fake close blocks like one behind the abandoned read's buffer lock). Fails when the close is awaited.test_a_busy_pool_closes_the_file_off_the_event_loop: when the transfer pool refuses the close, the file is closed on a thread of its own, never the loop (fails with a plainf.close()there).website/src/test/unscannedDownload.test.tsx: the dialog shows on 409 with the name and both sizes; "Download anyway" retries withconfirm_unscanned=1as a URL download, not a blob; Cancel and Escape download nothing; aFileDownloadLinkclick takes the same path; a small file still downloads with no dialog; the dialog uses the top layer so it paints above the full-screen preview; a Windows path (C:\work\report.docx) saves asreport.docx; a failed link download offers the agent hand-off.website/src/test/BinaryFileCard.test.tsx: a 404 Download on the file card shows its notice (office-card-download-error) in its own row, outside the Open / Download group, which keeps at most two controls. Fails when the card does not take the error.test/test_dashboard_files_composition_contract.py: shape digest recaptured forapi_file_download'sweb.StreamResponsereturn type.Proof: PROVEN test/test_file_download.py (proven on bd8c80759194; the review fixes amended since add tests of their own; the off-loop close test was run against the old close and fails)
Manual verification
On a pod built from this branch, with a 3 GB sparse file in the workspace:
GET /api/file-downloadanswered 409large_file_unscannedwithsize: 3221225472, limit: 52428800, a small file answered 200; right-click → Download in the chat Files tab showed the dialog below. The Electron shell's auto-save for the confirmed URL download was not exercised (Linux host, no desktop shell).Screenshots / video
Files tab → right-click a 3 GB file → Download, light and dark themes:
Related Issues
N/A — requested by the maintainer.
Sibling sweep
Swept: git grep -nE "_MAX_UPLOAD_BYTES|max_bytes=" -- src/kiro_crew/dashboard/file_api/ and git grep -n "file-download|fileDownloadUrl" -- website/src
Sibling: src/kiro_crew/dashboard/file_api/editor.py -- not affected -- file-raw is the inline viewer read, not a Download, and the brief keeps it capped
Sibling: website/src/components/FileRenderers.tsx -- changed here
Sibling: website/src/pierre/PierreWorkspaceTreeImpl.tsx -- changed here through the shared downloadFileToDisk helper, no edit needed
Sibling: website/src/apps/file-explorer/FileExplorerPage.tsx -- not affected -- its Download reads the Files app backend, not this route
Pattern harvest
Not generalizable: the cap came from one route borrowing the whole-read envelope; other readers keep that envelope on purpose.
Checklist
feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement
Contributions are accepted under the project's Apache-2.0 license; see the Licensing section of CONTRIBUTING.md.