Skip to content

fix(browser): repair isolated download seam#64558

Closed
Pearcekieser wants to merge 6 commits into
openclaw:mainfrom
Pearcekieser:fix-18598-downloads
Closed

fix(browser): repair isolated download seam#64558
Pearcekieser wants to merge 6 commits into
openclaw:mainfrom
Pearcekieser:fix-18598-downloads

Conversation

@Pearcekieser

@Pearcekieser Pearcekieser commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: isolated-browser downloads could fail at the browser-tool seam for two different reasons on the real path we exercised: browserAct(...) could hit loopback bridge auth before headers were resolved, and plain browser-tool clicks exposed Playwright artifact download paths instead of the managed OpenClaw downloads dir.
  • Why it matters: the browser tool reports a transient Playwright artifact path instead of the stable user-facing download location/filename semantics we expect from isolated browser downloads.
  • What changed: loopback browser auth resolution in client-fetch.ts is now lazy so the browser client can attach auth headers without a module-init seam issue; isolated launches tolerate a short CDP-readiness wobble instead of immediately tearing Chrome down; and raw page download events are patched to save into the managed downloads dir unless an explicit waiter is already handling the download.
  • What did NOT change: this does not broaden browser auth scope, change external download APIs, or keep the earlier “missing Chrome download prefs” theory alive.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: the browser-tool download path crossed an initialization seam where client-fetch.ts needed browser control auth while also importing modules that participated in the same startup graph, so loopback /act requests could miss auth and fail with Unauthorized. Separately, even when the click succeeded, plain browser-tool download events still surfaced Playwright artifact paths instead of the managed downloads dir because only the explicit wait/download helper path normalized the saved location.
  • Missing detection / guardrail: coverage only exercised the explicit helper-driven download flow, not the browser-tool click seam that uses /act and then consumes the raw Playwright download event.
  • Contributing context (if known): isolated Chrome startup can briefly look unhealthy at the immediate CDP probe boundary, which made the seam harder to reproduce consistently until the readiness wait was made more tolerant.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/browser/src/browser/pw-tools-core.downloads.browser-tool-seam.live.test.ts
  • Scenario the test should lock in: a plain browser-tool /act click against an isolated browser download should not return a Playwright artifact path and should land in the managed OpenClaw downloads dir with the expected filename/body.
  • Why this is the smallest reliable guardrail: the bug lives at the real browser-tool seam (/act + auth + isolated browser download event handling), not just inside a lower-level helper.
  • Existing test that already covers this (if any): none that covered the exact seam.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Browser-tool clicks against isolated browser downloads no longer expose Playwright artifact paths.
  • Loopback browser actions now resolve auth reliably through the real browser client path.
  • Isolated launches are less likely to flap on a brief CDP-readiness wobble during startup.

Diagram (if applicable)

Before:
[browserAct / loopback /act] -> [auth resolved at fragile init seam] -> [Unauthorized or flaky startup]
                                             |
                                             v
                              [download event returns Playwright artifact path]

After:
[browserAct / loopback /act] -> [lazy auth resolution + tolerant CDP readiness] -> [click succeeds]
                                                                           |
                                                                           v
                                         [download event is normalized into managed downloads dir]

Security Impact (required)

  • New permissions/capabilities? (Yes/No) No
  • Secrets/tokens handling changed? (Yes/No) No
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) No
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: macOS host
  • Runtime/container: local OpenClaw browser / isolated profile path
  • Model/provider: N/A
  • Integration/channel (if any): isolated browser (profile=openclaw-style flow via local bridge)
  • Relevant config (redacted): local bridge auth + isolated Chrome profile + managed download dir

Steps

  1. Start an isolated browser profile via the local bridge.
  2. Open a page with a plain <a href="/download"> attachment link.
  3. Trigger the link through the browser-tool /act click path.
  4. Observe the Playwright download event path returned to the caller.

Expected

  • The /act click works through the authenticated loopback browser client path.
  • The download resolves into the managed OpenClaw downloads dir with the expected filename/body.

Actual

  • Before fix on the real seam matrix:
    • committed branch before this patch: fails, returning a Playwright artifact path
    • origin/main: fails, returning a Playwright artifact path
    • this fix: passes, returning the managed downloads dir path
  • Additionally, the raw harness on origin/main showed the loopback browser client can still fail with Unauthorized through browserAct(...), which is the auth seam cleaned up here.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: ran the new seam live test on three states and confirmed the fail/fail/pass matrix (fix-18598-downloads@c59f857486 fail, origin/main@3c03d41f13 fail, fixed branch 3d11cda81b pass).
  • Edge cases checked: the lower-level browserAct(...) harness on origin/main also exposed the loopback auth seam as Unauthorized, confirming the earlier “download prefs only” story was incomplete.
  • What I did not verify: end-user desktop interactions in chrome://downloads, and the flaky broader browser-in-loop live variant was intentionally removed instead of being kept as misleading coverage.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: patching raw page download events could interfere with the explicit wait/download helper path.

    • Mitigation: the page state now tracks when the explicit waiter is active and skips the patch in that case.
  • Risk: making launch readiness more tolerant could mask real Chrome startup failures.

    • Mitigation: the fallback only keeps the process when Chrome is still running; dead/failed launches still tear down and throw.

@Pearcekieser Pearcekieser changed the title fix(browser): heal isolated profile download prefs fix(browser): repair isolated download seam Apr 15, 2026
@Pearcekieser
Pearcekieser marked this pull request as ready for review April 15, 2026 02:09
@greptile-apps

greptile-apps Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR repairs three interlocked issues in the isolated browser download path: lazy auth-dep loading in client-fetch.ts fixes loopback /act auth failures at the module-init seam; a tolerant CDP-readiness wait in server-context.availability.ts avoids tearing down Chrome on brief startup wobbles; and a new download event handler in ensurePageState saves raw page downloads into DEFAULT_DOWNLOAD_DIR when no explicit waiter is active, preventing Playwright artifact paths from leaking to callers.

  • The managedSave IIFE in pw-session.ts (lines 405–413) has no rejection handler attached to the promise itself. If download.saveAs() fails before any caller awaits patched.path(), Node.js 22 treats this as an unhandled rejection and will terminate the process with --unhandled-rejections=throw (the default). A .catch(() => {}) on managedSave suppresses the unhandled signal while still propagating the error through path().

Confidence Score: 4/5

Safe to merge after addressing the unhandled-rejection risk in the download event handler; the auth and CDP-readiness fixes are correct.

One P1 finding: the floating managedSave promise in pw-session.ts can produce an unhandled rejection that terminates the Node.js 22 gateway process if saveAs fails before path() is awaited. All other changes (lazy auth, CDP tolerance, profile decoration, new tests) are well-structured and correct.

extensions/browser/src/browser/pw-session.ts — the new download event handler needs a .catch(() => {}) on managedSave to suppress the unhandled-rejection risk.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/browser/src/browser/pw-session.ts
Line: 405-413

Comment:
**Unhandled rejection from `managedSave` IIFE**

`managedSave` is a floating promise — nothing attaches a rejection handler to the promise itself. Assigning `patched.path = async () => await managedSave` only handles the rejection when `path()` is actively called. If `download.saveAs()` fails before any caller awaits `path()` (e.g., disk full during `saveAs`, or the download is cancelled at the browser level immediately), Node.js 22 will emit `unhandledRejection` and, with the default `--unhandled-rejections=throw` mode, terminate the gateway process.

A minimal fix is to attach a no-op rejection handler to suppress the unhandled signal while still propagating the error to any caller who awaits `path()`:

```suggestion
      const managedSave = (async () => {
        await fs.mkdir(DEFAULT_DOWNLOAD_DIR, { recursive: true });
        await fs.rm(managedPath, { force: true });
        await download.saveAs(managedPath);
        return managedPath;
      })();
      // Suppress unhandled-rejection warnings: the error is still propagated to
      // any caller that awaits patched.path().
      managedSave.catch(() => {});
      const patched = download as typeof download & { path?: () => Promise<string> };
      patched.path = async () => await managedSave;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/browser/src/browser/pw-session.ts
Line: 396-413

Comment:
**Concurrent downloads with identical filenames will clobber each other**

`managedPath` is `DEFAULT_DOWNLOAD_DIR/{suggestedFilename}` — a flat, non-unique path. If two `page.on("download")` callbacks fire before either finishes (e.g., a page that triggers multiple downloads at once), both handlers will:

1. `fs.rm(managedPath, { force: true })` — interleave the deletions
2. `download.saveAs(managedPath)` — race to write the same file

The second `saveAs` could overwrite the first's file while the first caller still holds a reference to the path, or one `saveAs` could fail because the other just deleted the partially-written bytes via `rm`.

Consider generating a unique path (e.g., using a UUID prefix, similar to `buildTempDownloadPath` in `pw-tools-core.downloads.ts`) and then renaming/linking to `managedPath` when done, or at minimum documenting that concurrent same-name downloads are not supported here.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(browser): repair isolated download s..." | Re-trigger Greptile

Comment thread extensions/browser/src/browser/pw-session.ts
Comment thread extensions/browser/src/browser/pw-session.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d11cda81b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/browser/src/browser/server-context.availability.ts Outdated
Comment thread extensions/browser/src/browser/pw-session.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69d60f2924

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/browser/src/browser/pw-session.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f760fbbb88

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/browser/src/browser/pw-tools-core.downloads.ts Outdated
@Pearcekieser

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. 🎉

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@Pearcekieser Pearcekieser left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is ready for maintainer to look, trying to see if there is anything else I need to do to get the right tags etc added

@obviyus obviyus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into this. I think the isolated-profile download bug in #18598 looks real, but this PR still feels over-scoped for that issue: the Chrome download pref decoration looks plausibly relevant, while the lazy /act auth change and the Playwright download.path() patch read like separate seam fixes. Could you either split those pieces out, or add direct proof that they are required for the reported chrome://downloads filename/clickability bug on macOS?

@steipete

Copy link
Copy Markdown
Contributor

Codex deep review: this is the better/current-path download-seam fix, and it supersedes #48132.

Current main has explicit /wait/download and /download endpoints, but plain act:click downloads can still fall through the unmanaged Playwright download path. This PR fixes that in the right layer: profile download prefs plus a default page.on("download") handler in ensurePageState(), while explicit waiters are guarded by downloadWaiterDepth so we do not double-saveAs() user-requested downloads.

I rechecked the earlier Greptile concern against the current diff: the branch now uses unique managed paths and attaches managedSave.catch(() => {}), while still surfacing save failures through the patched download.path(). That addresses the unhandled-rejection and same-filename clobber risks.

Remaining scope note: this should not close #38519 by itself. It repairs the underlying unmanaged download seam, but #38519 asks for agent-facing download capture/metadata (or a tool-level download action) so downstream agent steps can reliably find the artifact from the tool response.

@steipete

Copy link
Copy Markdown
Contributor

Codex follow-up deep review: still the right successor to #48132, but this branch is no longer direct-mergeable.

Current main still lacks the core pieces this PR adds:

  • profile download prefs in the isolated Chrome profile;
  • a default unmanaged page.on("download") handler in ensurePageState(...);
  • a guard so explicit waitForDownloadViaPlaywright(...) / downloadViaPlaywright(...) flows do not double-save the same Playwright Download;
  • unique managed paths plus managedSave.catch(() => {}) so same-name downloads and failed saves do not crash the gateway.

So the bug class is real and the final shape in this PR is still good. The remaining issue is freshness: the branch is now ~3.6k commits behind main, and applying the patch to current main fails in chrome.profile-decoration.ts, chrome.test.ts, chrome.ts, pw-session.ts, and the download waiter test because those areas have moved.

Best path now:

I would preserve contributor credit if this lands via a maintainer rewrite.

@steipete

Copy link
Copy Markdown
Contributor

Codex maintainer rewrite landed on main as 41ed7fa (fix(browser): manage isolated downloads).

Kept the useful pieces from this PR and left out the unrelated auth/availability changes:

  • managed Chrome profiles now seed OpenClaw download prefs
  • unmanaged click-triggered downloads are saved under the guarded downloads directory
  • explicit waitfordownload / download waiters own the next download so the default handler does not race them
  • added focused regression coverage for profile prefs, unique unmanaged paths, rejection handling, and waiter ownership

Validation after rebase: pnpm check:changed and pnpm build passed. Closing this PR as superseded by the maintainer rewrite; keeping #38519 open for any broader remaining download UX work.

@steipete steipete closed this Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: macOS Sequoia: OpenClaw isolated Chrome profile cannot download CSV (chrome://downloads entry is unclickable + wrong filename)

3 participants