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:
- Passed a host file path →
BROWSER_RUN_API_UNSUPPORTED: File paths are unavailable in the QuickJS sandbox; use in-memory file payloads
- Tried
DataTransfer → QuickJS promise rejected: 'DataTransfer' is not defined
- Tried
Buffer.from() → QuickJS promise rejected: 'Buffer' is not defined
- Tried
TextEncoder → QuickJS 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):
Buffer → QuickJS promise rejected: 'Buffer' is not defined
- fell back to
btoa → QuickJS 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.js → 0. 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.
Summary
The
browser runQuickJS sandbox does not definebtoa,atob,TextEncoder,TextDecoder, orBuffer. 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 callsRetracted — see the correction below. The missing globals are the whole of this issue.btoaunconditionally, so any run reaching that path throws.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:
BROWSER_RUN_API_UNSUPPORTED: File paths are unavailable in the QuickJS sandbox; use in-memory file payloadsDataTransfer→QuickJS promise rejected: 'DataTransfer' is not definedBuffer.from()→QuickJS promise rejected: 'Buffer' is not definedTextEncoder→QuickJS promise rejected: 'TextEncoder' is not definedIt 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):
Buffer→QuickJS promise rejected: 'Buffer' is not definedbtoa→QuickJS promise rejected: btoa is not initializedAgent C:
QuickJS promise rejected: 'document' is not definedWhy they did that
browser run --helpsays "Run JavaScript with Playwright." The reference doc sayspage,context,browserandconsoleare "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 forBufferfor binary payloads andTextEncoderfor text — because that is what the Playwright docs forsetInputFilestell it to do.Agent A's own reasoning, quoted from its transcript:
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.
__webcmdEncodeBase64would have solved Agent A's task at step 2.Root cause
src/browser/run/runner.ts:340-382defines the helpers. The complete set of globals the sandbox exposes:No
btoa/atob/TextEncoder/TextDecoder/Buffer.Correction: the
utilityScriptSerializersclaim was wrongThis issue originally reported
src/browser/run/playwright-client/vendor/isomorphic/utilityScriptSerializers.ts:114as a livebtoacall that throws today. That is not true, verified empirically rather than by reading:grep -c btoa src/browser/run/generated/playwright-client.js→0. The file is vendored but imported by nothing on the client entry's graph, so the bundler drops it.quickjsEncoding.encodeBase64/decodeBase64(tBinary) and typed arrays throughserializers.tswith rawUint8Array. Neither touchesbtoa.maintoday: passingnew Uint8Array([1,2,3])intopage.evaluate, and returning one from its callback.The observed
btoa is not initializedcame 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:btoa/atoboperate on binary strings (latin1), not UTF-8, so do not compose them from__webcmdEncodeText. Implement by mappingcharCodeAtper character and throwingInvalidCharacterErroron any codepoint above 255.Bufferis 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/TextDecoderround-trip insidebrowser run, one asserting a known vector (btoa('hello') === 'aGVsbG8='), and one assertingbtoathrows above codepoint 255.A serializer round-trip test is worth keeping as a regression guard, but note it passes on
maintoo — per the correction above, there is no failing path to fix.