Skip to content

browser and computer tools in the sandbox - #36

Merged
jackeydou merged 2 commits into
mainfrom
browser-computer-use
Oct 8, 2026
Merged

jackeydou merged 2 commits into
mainfrom
browser-computer-use

Conversation

@jackeydou

Copy link
Copy Markdown
Owner

Summary

Agents can drive a web browser and a virtual screen inside their own sandbox (plan A in spec/2026-10-07-browser-computer-use). The sandbox still has no network: the browser reaches only loopback and files. The isolation defaults do not change.

  • Display image (deploy/images/display, mise run display:build): Xvfb, matchbox, xdotool, and Playwright 1.63's Chromium, plus swarm-display, a daemon and CLI that run as the user swarmdisplay. The screen needs an X cookie only that user can read, and the browser is driven through a pipe, with no debugging port open. An agent's own user cannot reach either one except through the tools.
  • sandboxd:
    • CreateSandboxRequest.display starts the display before the first manifest.
    • Processes running as the display's uid are left out of every process listing. This is by uid, not by process tree, because some Chromium helpers get reparented to pid 1.
    • ExecRequest.collect reads and removes files outside the key paths, as the call's user, and returns them as blobs.
    • Exec moves to exec.go, so service.go stays under 700 lines.
  • Runtime:
    • computer tool: mouse, keyboard, and screenshots, in screen pixels.
    • browser tool: actions on refs from Playwright's aria_snapshot(mode="ai"), with an optional screenshot.
    • Exec.user / Exec.collect, and SandboxTool.output.
    • Tool results carry ImageRefs, which are blob hashes. RequestOptions.max_images (default 3) limits a request to the context's latest images.
  • model-gateway: a tool message may carry inline PNG image_url parts. Before the request goes upstream, the gateway moves them into a user message after the tool messages. It does this after reasoning passback, so the within_turn boundary does not move.
  • Events and export:
    • Event schema 10 adds images, max_images, and collected. Older runs read as before.
    • The .eval export embeds the images. Tool results carry them inline; contexts reference them as attachments, with [image omitted] for images a request left out.
    • The transcript check rebuilds each request with its images.
  • Case format: env.yaml schema 2 adds a profile's display; version 1 still loads. browser and computer require a profile with a display.
  • Forks: a fork at a turn after the run's first display-tool call is refused with FAILED_PRECONDITION, because a fork restores files but not the browser.
  • Console: the case flow shows a profile's display.

Test plan

  • mise run check
  • mise run go:test-integration: TestLiveDisplayDrivesTheBrowserAndKeepsItsProcessesOut runs against the real display image, and the rest of the sandboxd suite passes.
  • mise run test:docker: tests/worker/test_display_e2e.py runs the whole platform:
    • screenshots reach the mock model backend;
    • the exported .eval resolves them;
    • a fork before the first display call runs to completion, and a fork after it is refused.
  • Isolation check by hand: neither the agent's user nor capability-less root can open the X display or /run/swarm-display.
  • Run under gVisor (runsc). Only runc on arm64 was available here, so Chromium under runsc still needs checking on the Linux host.

Notes

  • docs/services/orchestrator.md was already over 700 lines (794); this PR adds two.
  • Console replay does not render screenshots yet. They are in the events and in the .eval.
  • In a full test:docker run, test_takeover.py::test_a_live_worker_keeps_its_run_past_many_leases failed once. It uses 0.3 s leases and failed while the Go integration tests were running at the same time; it passes on its own.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T23:31:21.780186Z 073c739 Manual request
🔒 Security Review ✅ Completed 2026-10-07T23:04:28.798749Z ca06d11 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…er in the sandbox

Agents drive a web browser and a virtual screen inside their own sandbox
(spec/2026-10-07-browser-computer-use). The sandbox keeps no network: the
browser reaches loopback and files only, and isolation defaults are
unchanged.

- deploy/images/display builds swarmeval/display: Xvfb, matchbox,
  xdotool, Playwright 1.63's Chromium, and `swarm-display`, a daemon and
  CLI run as the user `swarmdisplay`. Agents' own users cannot reach the
  screen or the browser except through the tools.
- sandboxd: CreateSandboxRequest.display starts the display before the
  first manifest; the display user's processes are left out of every
  listing, so renderers are neither reported nor make changes ambiguous.
  ExecRequest.collect reads and removes files outside the key paths as
  the call's user and returns them as blobs. Exec moves to exec.go.
- runtime: `computer` (screen pixels) and `browser` (accessibility
  snapshot refs) tools; Exec.user, Exec.collect, SandboxTool.output.
  Tool results carry ImageRefs; RequestOptions.max_images (default 3)
  keeps a request to the latest images.
- model-gateway: tool messages may carry inline PNG image parts, moved
  into a user message after the tool messages, after reasoning passback.
- Event schema 10 (`images`, `max_images`, `collected`); older runs read
  as before. The .eval export embeds the images; the transcript check
  rebuilds requests with them.
- env.yaml schema 2 adds a profile's `display`; `browser` and `computer`
  need one. A fork after a run's first display call is refused.
- console: the case flow shows a profile's display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jackeydou
jackeydou force-pushed the browser-computer-use branch from ca06d11 to 073c739 Compare October 7, 2026 23:08
@jackeydou

Copy link
Copy Markdown
Owner Author

@codex review

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

Copy link
Copy Markdown

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: 073c7398f8

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +62 to +64
for plan in sandboxes.values():
if env.sandbox_profiles[plan.profile].display is not None:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject agents configured as the display user

When a profile has a display, this early continue skips every agent-level check, including os_user. Because the display image already defines swarmdisplay and sandboxd preserves image users, a case can set os_user: swarmdisplay; the agent's shell calls then run with access to the X cookie, daemon socket, screenshots, and browser profile under /run/swarm-display. This lets it manipulate or inspect the browser without the browser/computer tool and image-event path, so agents assigned to display sandboxes must be forbidden from using this reserved account.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 75ab13d. The loader refuses os_user: swarmdisplay. With a display, sandboxd also refuses that user among the sandbox users. After it starts the display, it checks the uid a command run without a user gets, so an image built FROM swarmeval/display whose USER is swarmdisplay fails the sandbox too. I tested that against a real image with USER swarmdisplay.

Comment on lines +282 to +287
if req.Display != nil {
uid, err := s.startDisplay(ctx, sb, req.Display)
if err != nil {
return errors.Join(fmt.Errorf("sandbox %s of run %s: %w", req.SandboxID, req.RunID, err), s.drv.Remove(context.WithoutCancel(ctx), id))
}
sb.displayUID = uid

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Start forked displays after restoring sandbox files

For the explicitly allowed case of a fork before the first display-tool call, sandbox creation reaches this block before worker/run.py restores the source files. Consequently the start.d applications and initial browser URL consume the case's seed files, and only afterward are the mounted key paths replaced with fork state; an application that loads state at startup or the already-loaded page can therefore expose stale seed-time data on the fork's first browser call. Display startup needs to occur after RestoreFiles, or the display stack must be restarted after restoration.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Keeping this order on purpose. When the source sandbox was created, its display (start.d apps and first page) started from the seed files, and the agent's file changes came after. A fork before the first display call replays that: start from the seed files, then RestoreFiles brings the files to the source's state at the fork point. If the display started after the restore, an app that loads state at startup would begin from fork-point files the source app never started from, which is less faithful. In-memory state a process built from later file changes is not restored for any process. That is the existing fork rule ("background processes never come back"). The ordering is now documented in docs/case-format.md#display (75ab13d).

An agent with `os_user: swarmdisplay` ran its shell as the display's
user, with the X cookie, the daemon's socket, and the browser profile in
reach, around the `browser` and `computer` tools. The loader now refuses
that os_user. sandboxd, with a display, refuses it among the sandbox's
users, and after starting the display checks the uid a command without a
user gets: an image whose `USER` is `swarmdisplay` fails the sandbox.

The case format says why a fork before the first display call starts the
display before restoring files: in the source, too, the display started
from the seed files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jackeydou
jackeydou merged commit a20d77d into main Oct 8, 2026
1 check passed
@jackeydou
jackeydou deleted the browser-computer-use branch October 8, 2026 00:28
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