Skip to content

browser run: QuickJS sandbox lacks btoa/atob/TextEncoder/TextDecoder while working equivalents exist as private __webcmd* globals #335

Description

@ankitranjan7

Summary

The browser run QuickJS sandbox does not define btoa, atob, TextEncoder, TextDecoder, or Buffer. Working implementations of exactly these operations already exist in the sandbox — but under private-looking names (__webcmdEncodeBase64, __webcmdDecodeBase64, __webcmdEncodeText, __webcmdDecodeText) that are documented nowhere and signal "internal, do not touch."

Separately and more seriously: vendored Playwright code calls btoa unconditionally, so any run reaching that path throws. Retracted — see the correction below. The missing globals are the whole of this issue.

What agents actually do

Observed in an internal agent-behaviour eval. Three separate agents, on three unrelated tasks, each burned multiple turns rediscovering the same absence:

Agent A (upload a file to a converter page), four consecutive failures:

  1. Passed a host file path → BROWSER_RUN_API_UNSUPPORTED: File paths are unavailable in the QuickJS sandbox; use in-memory file payloads
  2. Tried DataTransferQuickJS promise rejected: 'DataTransfer' is not defined
  3. Tried Buffer.from()QuickJS promise rejected: 'Buffer' is not defined
  4. Tried TextEncoderQuickJS promise rejected: 'TextEncoder' is not defined

It then gave up on the real page, detoured to about:blank, reimplemented the conversion by hand, and produced a file that did not match the expected output.

Agent B (extract data behind a generated page):

  • BufferQuickJS promise rejected: 'Buffer' is not defined
  • fell back to btoaQuickJS promise rejected: btoa is not initialized

Agent C: QuickJS promise rejected: 'document' is not defined

Why they did that

browser run --help says "Run JavaScript with Playwright." The reference doc says page, context, browser and console are "normal Playwright globals." Both descriptions are consistent with a Node-hosted Playwright script, which is what every public Playwright example on the internet looks like. An agent reasoning from that description will reach for Buffer for binary payloads and TextEncoder for text — because that is what the Playwright docs for setInputFiles tell it to do.

Agent A's own reasoning, quoted from its transcript:

Looking at this more carefully: the QuickJS sandbox wraps Playwright's API. setInputFiles accepts { name, mimeType, buffer } but the sandbox says Buffer is not defined. Let me try using Uint8Array instead which is standard JS.

That is correct reasoning against a runtime whose actual shape is undiscoverable. The agent is not guessing randomly — it is walking the standard-library ladder downward, and every rung is missing.

The failure is not that these globals are absent. It is that the capability is present under a name nothing points to. __webcmdEncodeBase64 would have solved Agent A's task at step 2.

Root cause

src/browser/run/runner.ts:340-382 defines the helpers. The complete set of globals the sandbox exposes:

browser, context, page
__webcmdEncodeBase64, __webcmdDecodeBase64
__webcmdEncodeText,   __webcmdDecodeText
__webcmdWriteArtifact, __webcmdRun, __webcmdTransportReceive,
__webcmdInitializePlaywright, __webcmdCancelPlaywright

No btoa/atob/TextEncoder/TextDecoder/Buffer.

Correction: the utilityScriptSerializers claim was wrong

This issue originally reported src/browser/run/playwright-client/vendor/isomorphic/utilityScriptSerializers.ts:114 as a live btoa call that throws today. That is not true, verified empirically rather than by reading:

  • grep -c btoa src/browser/run/generated/playwright-client.js0. The file is vendored but imported by nothing on the client entry's graph, so the bundler drops it.
  • The bundle serializes binary through quickjsEncoding.encodeBase64/decodeBase64 (tBinary) and typed arrays through serializers.ts with raw Uint8Array. Neither touches btoa.
  • Both directions round-trip correctly on main today: passing new Uint8Array([1,2,3]) into page.evaluate, and returning one from its callback.

The observed btoa is not initialized came from user program code calling the missing global — which is exactly the primary finding above, and is unaffected. There is no separate latent bug to fix.

Fix

Alias the existing implementations to their standard names, in the same shim block in runner.ts:

globalThis.btoa = value => __webcmdEncodeBase64(__webcmdEncodeText(value));
globalThis.atob = value => __webcmdDecodeText(__webcmdDecodeBase64(value));
globalThis.TextEncoder = class { encode(v) { return __webcmdEncodeText(v); } };
globalThis.TextDecoder = class { decode(b) { return __webcmdDecodeText(b); } };

btoa/atob operate on binary strings (latin1), not UTF-8, so do not compose them from __webcmdEncodeText. Implement by mapping charCodeAt per character and throwing InvalidCharacterError on any codepoint above 255.

Buffer is deliberately Node-only and should stay absent; issue #336 covers pointing agents at the right replacement when they reach for it.

Check

One test asserting btoa/atob/TextEncoder/TextDecoder round-trip inside browser run, one asserting a known vector (btoa('hello') === 'aGVsbG8='), and one asserting btoa throws above codepoint 255.

A serializer round-trip test is worth keeping as a regression guard, but note it passes on main too — per the correction above, there is no failing path to fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions