You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
page.waitForEvent("download") reports join(downloadDir, suggestedFilename) as the download path, but under behavior: "allow" the browser chooses the on-disk name itself: it sanitizes suggestedFilename per platform (path separators everywhere; :?"<>|*, reserved device names, and trailing dots/spaces on Windows) and deduplicates collisions with " (1)" suffixes. Whenever any of that fires, download.path() points at a file that was never written and saveAs() fails with ENOENT. This PR switches the primary path to Chromium's allowAndName behavior — the same scheme Playwright uses — so the on-disk name is the download guid and the reported path can no longer drift from it.
Related issue
No open issue. When closing #130/#132, @WUXM5 noted that Playwright "uses GUID-based naming together with path containment checks" and that additional boundary hardening might be addressed separately — this implements exactly that design. The user-visible defect it fixes (path drift under sanitization/deduplication) is distinct from the traversal claim that #130 investigated and rejected.
Changes
src/driver/downloads.ts — Browser.setDownloadBehavior now requests behavior: "allowAndName", so the browser writes the file as its download guid inside the per-download temp dir; download.path() returns that guid path. suggestedFilename() is unchanged and still reports the server-suggested name; saveAs(target) still copies to the caller's chosen name.
The Page.setDownloadBehavior fallback (runtimes without the Browser-level command) keeps behavior: "allow" — the Page-level command does not support allowAndName — and continues deriving the path from suggestedFilename there, as does the primary path if an event ever arrives without a guid.
src/driver/downloads.test.mjs — updated facade/fallback assertions plus two new tests: missing-guid derivation, and rejection of an escaping composed path.
Verification
Run from package/ego-browser (Windows 11, Node 24; suite is platform-neutral):
npm test -> 313 pass, 0 fail (3 download tests before, 5 after)
npm run typecheck -> ok
npm run validate:site-skills -> site skills ok
npm audit --audit-level=moderate -> 0 vulnerabilities
Revert-check: with src/driver/downloads.ts reverted and the new tests kept, "returns a Playwright-style download facade" (guid naming) and "rejects a path that escapes the download directory" both fail; with the fix they pass.
Real-browser check I could not run here (no macOS install): a download whose Content-Disposition filename contains a character the platform sanitizes (for example report:v2.pdf on macOS via a colon on Windows-style checks, or any name that collides with an existing file so the browser writes file (1).png) — before this change download.path() names the unsanitized/original variant, after it names the guid file the browser actually wrote. The existing scripts/real-browser-e2e/cases/downloads.mjs case covers the happy path end-to-end and is worth a run on macOS before merge.
Impact
Public helper API or behavior
Agent skill or instructions
Site learning
Installation or update flow
Build, CI, or release process
Documentation only
No externally visible impact
download.path() now returns a guid-named file instead of a suggestedFilename-named file. That matches Playwright's contract (agents are told to use suggestedFilename() for the display name and saveAs() for a stable destination, both unchanged), so scripts written against Playwright semantics keep working; only code that assumed the temp-path basename equals the suggested name would notice — and that assumption is exactly what breaks today under sanitization or dedup.
Checklist
The PR targets the correct base branch (dev for normal changes; only dev may target main).
The change is focused and does not include unrelated cleanup.
Tests were added or updated for behavior changes, or the reason they are unnecessary is explained above.
Relevant tests and validation commands pass locally.
Public helper JSDoc and agent-facing documentation are updated when the helper surface changes.
No credentials, tokens, cookies, personal data, or other secrets are included.
Flagging this myself before it costs anyone review time: I think this PR is superseded on the sprint-1.3.0 line.
That branch adds playwright-core@1.52.0 as a runtime dependency and removes src/driver/ entirely, so the src/driver/downloads.ts this PR modifies no longer exists there. Playwright's own download implementation already does both things this PR argues for — GUID-based on-disk naming and a containment check on the reported path — which is the design @WUXM5 described when closing #132, so adopting Playwright wholesale gets there more thoroughly than my patch does.
The PR is still green and mergeable against dev (where src/driver/downloads.ts is present and the drift bug is real), so I have left it open rather than closing unilaterally — dev is the base CONTRIBUTING documents. Happy to close it immediately if sprint-1.3.0 is the line that ships; just say so, or I will close it once that work reaches dev/main.
Same situation applies to #251 and #252, which touch src/video-recorder.ts and src/driver/screencast.ts — both also absent on sprint-1.3.0.
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
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
page.waitForEvent("download")reportsjoin(downloadDir, suggestedFilename)as the download path, but underbehavior: "allow"the browser chooses the on-disk name itself: it sanitizessuggestedFilenameper platform (path separators everywhere;:?"<>|*, reserved device names, and trailing dots/spaces on Windows) and deduplicates collisions with " (1)" suffixes. Whenever any of that fires,download.path()points at a file that was never written andsaveAs()fails with ENOENT. This PR switches the primary path to Chromium'sallowAndNamebehavior — the same scheme Playwright uses — so the on-disk name is the download guid and the reported path can no longer drift from it.Related issue
No open issue. When closing #130/#132, @WUXM5 noted that Playwright "uses GUID-based naming together with path containment checks" and that additional boundary hardening might be addressed separately — this implements exactly that design. The user-visible defect it fixes (path drift under sanitization/deduplication) is distinct from the traversal claim that #130 investigated and rejected.
Changes
src/driver/downloads.ts—Browser.setDownloadBehaviornow requestsbehavior: "allowAndName", so the browser writes the file as its download guid inside the per-download temp dir;download.path()returns that guid path.suggestedFilename()is unchanged and still reports the server-suggested name;saveAs(target)still copies to the caller's chosen name.Page.setDownloadBehaviorfallback (runtimes without the Browser-level command) keepsbehavior: "allow"— the Page-level command does not supportallowAndName— and continues deriving the path fromsuggestedFilenamethere, as does the primary path if an event ever arrives without a guid.suggestedFilenamebefore emitting the events, so a well-behaved runtime never trips it; it hardens the boundary against an event source that does not sanitize (the defense-in-depth gap acknowledged in Security: path traversal in download facade via server-controlled suggestedFilename (arbitrary local file read) #130).src/driver/downloads.test.mjs— updated facade/fallback assertions plus two new tests: missing-guid derivation, and rejection of an escaping composed path.Verification
Run from
package/ego-browser(Windows 11, Node 24; suite is platform-neutral):Revert-check: with
src/driver/downloads.tsreverted and the new tests kept, "returns a Playwright-style download facade" (guid naming) and "rejects a path that escapes the download directory" both fail; with the fix they pass.Real-browser check I could not run here (no macOS install): a download whose
Content-Dispositionfilename contains a character the platform sanitizes (for examplereport:v2.pdfon macOS via a colon on Windows-style checks, or any name that collides with an existing file so the browser writesfile (1).png) — before this changedownload.path()names the unsanitized/original variant, after it names the guid file the browser actually wrote. The existingscripts/real-browser-e2e/cases/downloads.mjscase covers the happy path end-to-end and is worth a run on macOS before merge.Impact
download.path()now returns a guid-named file instead of asuggestedFilename-named file. That matches Playwright's contract (agents are told to usesuggestedFilename()for the display name andsaveAs()for a stable destination, both unchanged), so scripts written against Playwright semantics keep working; only code that assumed the temp-path basename equals the suggested name would notice — and that assumption is exactly what breaks today under sanitization or dedup.Checklist
devfor normal changes; onlydevmay targetmain).fix).