Skip to content

feat(browser): expose writeArtifact under a public name and document it - #345

Merged
ankitranjan7 merged 1 commit into
fix/browser-run-globalsfrom
feat/browser-run-write-artifact
Aug 18, 2026
Merged

feat(browser): expose writeArtifact under a public name and document it#345
ankitranjan7 merged 1 commit into
fix/browser-run-globalsfrom
feat/browser-run-write-artifact

Conversation

@ankitranjan7

Copy link
Copy Markdown
Contributor

Closes #339.

Stacked on #344 (fix/browser-run-globals). Base is that branch, not main — the docs example uses TextEncoder, which #344 adds, and both PRs edit the same shim block. Review/merge #344 first.

What

__webcmdWriteArtifact was the only way to get bytes out of the QuickJS sandbox, and its dunder prefix reads as "private implementation detail" to anything that finds it. An internal agent-behaviour eval showed an agent capture a download correctly, fail to find any way to persist it, then fall back to scraping the on-page preview and hand-writing a mismatched file.

  • globalThis.writeArtifact(filename, bytes, contentType?) is now the public name. Bare global, matching the flat page/context/browser precedent — a webcmd.* namespace would be a second discovery hop for one function, and the sandbox has no other namespaced surface to hang it off.
  • __webcmdWriteArtifact stays as a direct alias to the same function, so in-flight programs and the internal page.screenshot({ path }) wrapper are unaffected.
  • It now returns the receipt. The old implementation did await __webcmdHostCall(...) with no return, so the receipt the host already produced was discarded. browser run: __webcmdWriteArtifact is the only way out of the sandbox and is documented nowhere #339's check requires the receipt back, and the screenshot wrapper never needed it, which is why nothing caught this.
  • It accepts Uint8Array or anything new Uint8Array(...) accepts.

Bug found while testing the two-argument call

Calling writeArtifact(name, bytes) without a contentType failed with BROWSER_RUN_INVALID_INPUT. JSON.stringify([f, b, undefined]) yields [f, b, null], and the host guard tested args[2] !== undefined && typeof args[2] !== 'string', so null fell through to the rejection. Loosened to args[2] != null; the downstream default was already args[2] ?? artifactContentType(...), which handles null correctly. Only the screenshot wrapper called this before, and it always passes a content type, so the two-argument path had never run.

Docs

skills/webcmd-browser/references/browser-run-playwright.md gains an "Artifacts: getting bytes out of the sandbox" section covering the signature, the download path, and receipt redemption.

That file is being rewritten in #342. This PR branches off main's version rather than #342's, so it does not carry that rewrite — but the section is written in #342's structure and tone (short prose, fenced examples, worked cases) so it slots straight into its "Files and binary data" area on merge.

Two things I verified in the sandbox before writing the example, rather than assuming:

  • download.createReadStream() throws Readable streams are not available in the QuickJS sandbox. This is the obvious thing to reach for after catching a download event, and it is a dead end.
  • download.saveAs('relative/name.csv') already works and routes through the artifact sink, producing a receipt in output.artifacts with no writeArtifact call at all. That is the correct answer for the download case and was documented nowhere. The docs now lead with it.

src/skills.test.ts asserted the old /artifact paths/i heading; retargeted to /artifacts/i plus a literal writeArtifact( check, which is a stronger assertion than the heading text.

Tests

returns a redeemable receipt from %s, parameterized over writeArtifact and __webcmdWriteArtifact, in src/browser/run/runner.test.ts. Each writes through a LocalBrowserRunArtifactSink pointed at a temp dir, asserts the returned receipt's shape, asserts it equals the entry in output.artifacts, and reads the bytes back off disk at <baseDir>/<artifactId>/<filename> after run returns. Uses a nested logical path and a multi-byte character so the byte count is a real check.

Counts

npm run build clean.

npx vitest run --project unit:

passed skipped failed
main, no Chromium installed (stated baseline) 2457 57 0
main, Chromium installed 2513 1 0
#344 (this PR's base) 2518 1 0
this branch 2520 1 0

runner.test.ts is gated on a local Chromium binary, which is why the stated 2457 baseline shows 57 skips. I installed Chromium via node_modules/playwright-core/cli.js install chromium so these tests actually execute. The +2 over #344 is exactly the two parameterized cases above.

Cloud

No change needed. webcmd-cloud imports runBrowserProgram from @agentrhq/webcmd/browser/run at src/browser/hosted-browser.ts:17, so hosted runs get writeArtifact when the cloud bumps its pinned @agentrhq/webcmd (currently 0.7.2 on its main) — not bumped here.

This PR adds a sandbox global and loosens a host-side argument guard; it does not change the BrowserRunArtifactSink interface. webcmd-cloud/src/browser/run-artifact-sink.ts implements exactly that interface — write({ filename, contentType, bytes }) => BrowserRunArtifactReceipt — and is unaffected. Its receipts already flow back through the same output.artifacts array, with a cloud-artifact:// locator instead of browser-run://, which the new docs note.

🤖 Generated with Claude Code

__webcmdWriteArtifact was the only write path out of the QuickJS sandbox
and its dunder prefix reads as private, so programs would not call it.
Expose the same function as globalThis.writeArtifact, keep the dunder as
an alias, and return the receipt instead of swallowing it.

Also accept a null contentType, which is what JSON.stringify produces for
the two-argument call, and document artifacts in the browser-run reference
including the download path and how a receipt is redeemed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🟢 No documentation gap found — medium confidence

The automated review found no documentation gap in the supplied changes.

This review is advisory and does not block merging.

@ankitranjan7
ankitranjan7 merged commit 3ea0fe5 into fix/browser-run-globals Aug 18, 2026
34 checks passed
ankitranjan7 added a commit that referenced this pull request Aug 18, 2026
#344)

* fix(browser): add btoa/atob/TextEncoder/TextDecoder to the run sandbox

The QuickJS sandbox for `browser run` exposed working base64 and UTF-8
codecs only under private-looking `__webcmd*` names, so programs reaching
for the standard globals hit `'TextEncoder' is not defined`.

Alias them to the platform names in the existing platform shim. btoa/atob
are implemented as latin1 binary-string codecs rather than composed with
the UTF-8 helpers, which would corrupt binary input. Buffer stays absent —
it is Node-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(browser): expose writeArtifact under a public name and document it (#345)

__webcmdWriteArtifact was the only write path out of the QuickJS sandbox
and its dunder prefix reads as private, so programs would not call it.
Expose the same function as globalThis.writeArtifact, keep the dunder as
an alias, and return the receipt instead of swallowing it.

Also accept a null contentType, which is what JSON.stringify produces for
the two-argument call, and document artifacts in the browser-run reference
including the download path and how a receipt is redeemed.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Feat/evals (#349)

* benchmakrs added

* fix: removed missing refs from readme

* feat(benchmarks): support OpenAI as judge provider

* fix: screenshot upload

* feat: added cost support

* docs: design benchmark-specific judge prompts

* feat(benchmarks): split general and stealth judge prompts

* Delete docs/superpowers/specs directory

* chore: bump version metadata for 0.7.0 release

* Revert "chore: bump version metadata for 0.7.0 release"

This reverts commit c69011f.

* feat(benchmarks): expose full Webcmd skill pack to Codex evals

* feat(benchmarks): clarify stealth captcha handling

* fix: added support for webcmd browser skills only

* feat(benchmarks): run eval attempts in parallel

* fix(benchmarks): isolate parallel webcmd sessions

* test(benchmarks): cover parallel webcmd sessions

* pi support (#313)

* chore(benchmarks): pin webcmd 0.7.1

* refactor: disable concurrency in benchmark evaluation and remove integration tests

* feat(benchmarks): support Codex subscription evals

* docs(skills): improve browser diff and form guidance

* docs(skills): sync browser skill sources

---------

Co-authored-by: beubax <tejasr@bu.edu>
Co-authored-by: Tejas <48682479+beubax@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Rishabh <33702025+rishabhraj36@users.noreply.github.com>
Co-authored-by: beubax <tejasr@bu.edu>
Co-authored-by: Tejas <48682479+beubax@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant