Skip to content

SDK integration tests, auto-generated docs, and snapshot support - #1242

Merged
ddupont808 merged 45 commits into
mainfrom
sdk-tests-and-docs
Mar 30, 2026
Merged

SDK integration tests, auto-generated docs, and snapshot support#1242
ddupont808 merged 45 commits into
mainfrom
sdk-tests-and-docs

Conversation

@ddupont808

@ddupont808 ddupont808 commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Auto-generated reference docs: scripts/gen_interface_docs.py introspects all interface classes and emits interfaces.mdx with proper MDX escaping (HTML entities for <>{} outside code spans)
  • Comprehensive test coverage: integration tests for every Image builder method (apt, pip, env, run, copy, expose, from_registry, from_file, apk_install, pwa_install) and every sandbox interface (shell, mouse, keyboard, screen, clipboard, tunnel, terminal, window, mobile)
  • Snapshot support: cloud snapshot API (sb.snapshot()Image), snapshot guide docs, and test cases for Linux + Android snapshot workflows covering create/fork/isolation
  • Partial local snapshot support: speeds up local test runs by reusing snapshotted state instead of reinstalling packages each time
  • Cross-platform fixes: Android PWA package name sanitization, clipboard content extraction, drag path format, macOS env var propagation, Docker layer execution, QEMU compat detection

Test plan

  • pytest tests/test_image_builder.py -v — image builder tests against Docker
  • pytest tests/test_interfaces.py -v — interface tests against Docker
  • CUA_API_KEY=... pytest tests/test_snapshots.py -v — snapshot tests against cloud
  • pnpm -C docs build — docs build passes without MDX errors

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added sandbox snapshot functionality to capture running sandbox state and create reusable images
    • Added runtime compatibility checks to verify local machine can execute sandboxes
    • Extended API support for checkpoint/fork operations
  • Documentation

    • Comprehensive interfaces reference with updated method signatures and examples
    • New snapshots guide covering use cases, parameters, and performance expectations
  • Bug Fixes & Improvements

    • Enhanced cross-platform environment variable and file handling
    • Improved network layer implementation for better stability

ddupont808 and others added 30 commits March 26, 2026 19:53
…ypes from meta pkg

- New `cua_sandbox/runtime/compat.py` with `check_local_support(image) -> RuntimeSupport`
  that detects whether the image's runtime is installed/auto-installable and whether
  hardware acceleration (HVF, KVM, Hyper-V) is available for the host/guest OS+arch combo
- `Image.local_support()` method delegates to check_local_support (lazy import, no circulars)
- `skip_if_unsupported(image)` pytest helper replaces all hardcoded skipif/env-var gates
- `cua_sandbox/__init__` exports RuntimeSupport, check_local_support, skip_if_unsupported
- `cua` meta package: eager compat re-export, lazy __getattr__ for all runtime classes
  (DockerRuntime, QEMURuntime, LumeRuntime, AndroidEmulatorRuntime, HyperVRuntime, RuntimeInfo)
  and interface types (Shell, CommandResult, Mouse, Keyboard, Screen, Clipboard,
  Tunnel, TunnelInfo, Mobile, Terminal, Window) — no import-time overhead
- `cua/pyproject.toml`: add uv.sources pointing to local editable siblings
- Updated test_android_multitouch.py to use skip_if_unsupported instead of manual checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…CG on Apple Silicon

QEMU supports HVF (-accel hvf) for x86_64 guests on Intel Macs.
On Apple Silicon, x86_64 guests must use TCG software emulation (no cross-arch HVF).

- Replace _has_hvf() with _has_hvf_for_arm64_guest / _has_hvf_for_x86_guest
- Add _x86_guest_hw_accel() covering KVM (Linux), HVF (Intel Mac), Hyper-V (Windows)
- Linux VM and Windows VM sections now use _x86_guest_hw_accel()
- Android: Intel Mac gets HVF for x86_64 Android system images
- Apple Silicon correctly reported as software-only for Windows/Linux VMs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- tests/test_interfaces.py — 117 tests covering every sb.* interface method
  (shell, clipboard, screen, mouse, keyboard, tunnel, terminal, window, mobile)
  across Linux container, Linux VM, macOS VM, Windows VM, Android VM
- tests/test_image_builder.py — 37 tests covering every Image builder method
  (apt/brew/choco/winget/apk/pwa/pip/uv install, run, env, copy, expose,
  from_registry) across all guest OS types
- tests/pytest.ini — add pythonpath so `from cua import ...` resolves
- scripts/gen_interface_docs.py — auto-generate interfaces.mdx from pydocstrings
- docs/.../interfaces.mdx — regenerated from source (replaces AI-generated copy)

All tests use local=True + skip_if_unsupported(image) — no hardcoded skips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SSH sessions on macOS often have a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin)
that omits /usr/local/bin where OrbStack/Docker Desktop install the docker CLI.
_has_docker() now falls back through common install locations so Linux tests
don't incorrectly skip on machines where Docker is installed but not in PATH.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… modules

Instead of reimplementing binary/path detection in compat.py (which diverged
from the actual runtime logic), each _has_*() probe now calls into the
runtime module that owns that detection:

  _has_docker()     → docker._has_docker()       (moved multi-path logic here)
  _has_lume()       → lume._has_lume()
  _has_qemu()       → qemu_installer.qemu_bin()  (now finds Homebrew/MacPorts/cached)
  _has_android_sdk()→ android_emulator._sdk_path()
  _has_java()       → android_emulator._java_env()
  _has_hyperv()     → hyperv._has_hyperv()

Also promotes docker._has_docker() to use the same multi-path probe that
compat.py introduced, so the runtime itself finds Docker in stripped SSH
PATH environments (e.g. OrbStack on macOS via SSH).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All subprocess calls in DockerRuntime now use _docker_bin() which probes
common install locations (/usr/local/bin, /opt/homebrew/bin, etc.) so
the runtime works in SSH sessions where PATH is stripped (e.g. cua.local).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tarts

DockerRuntime.start() was missing layer execution — apt_install, pip_install,
run, env, etc. layers were stored in image._layers but never applied for
Docker containers (unlike VMs where create_session_disk() handles this).
Now uses LayerExecutor to apply layers via the running computer-server API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The computer-server /cmd endpoint expects params as {"params": {"command": ...}}
not top-level "command_args". Also fix result parsing to check "return_code"
(server field name) before "returncode", and use "success" flag for failure
detection when return_code is absent (e.g. on handler exceptions).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- executor: fix run/env layers to use sudo for system access
- executor: fix uv_install for Linux (uv pip install --system)
- executor: fix pip_install for Linux (--break-system-packages)
- executor: implement copy layer via computer-server write_bytes
- executor: implement expose layer as no-op (handled at docker run)
- executor: add os_type param to select correct commands per platform
- docker: handle image._env and image._files after container starts
- docker: map image._ports as additional -p flags at docker run time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Environment variables from image.env() are now passed as -e KEY=VAL
at docker run time, ensuring they're available in subprocess shells.
Previously they were written to /etc/environment post-start which
doesn't work since subprocesses don't source that file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…om_registry

- docker: apply _files before _layers so copy → run(chmod) ordering works
- docker: write env vars to /etc/profile.d/cua-env.sh for sudo access
- docker: fix expose port range (port to port+1000 not 8000-9000)
- executor: source /etc/profile.d/cua-env.sh in run layer bash invocation
- tests: fix from_registry tests to use cua image (needs computer-server)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… expose test

- executor: write copy files to /tmp first then sudo mv for root-owned paths
- docker: write env profile via sudo tee (direct write_bytes can't write to /etc/profile.d)
- tests: simplify expose test — just verify container starts, not socket internals

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lume's Swift HTTP server closes the TCP connection after sending a 4xx
response (e.g. VM-not-found on GET /lume/vms/{name}). When httpx reuses
that connection for the subsequent POST /lume/pull/start, the body arrives
empty at the server, causing 'Invalid request body' 400 errors.

Fix: open a fresh AsyncClient for the pull POST instead of reusing the
client that made the initial VM-status GET.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LumeRuntime.start() was returning without applying _layers, _env, or
_files — unlike DockerRuntime which runs LayerExecutor after is_ready().
Add _apply_image_layers() helper and call it in both start() return paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sandbox.destroy(): call runtime.delete() instead of stop() for ephemeral
  sandboxes, so lume VMs are permanently removed (not just stopped)
- executor._exec_run: don't use sudo on macOS/Android VMs — macOS VMs
  require a password for sudo; only Linux containers have passwordless sudo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
macOS lume VMs have the default password 'lume'. Pipe it to sudo -S
so run layers and env var setup can write to system paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lume's custom HTTP server uses minimumIncompleteLength=1, so on a fresh
TCP connection the request body can arrive after the first receive()
returns, causing a spurious 400. Add _pull_start_with_retry() that
retries up to 3 times with a 1s pause between attempts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tart

Writing to /etc/profile.d/cua-env.sh is only sourced by login shells on
macOS. Instead write to ~/.zshenv (sourced by all zsh invocations) and
set via launchctl setenv, then restart computer-server so it inherits
the new environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
computer-server's launchd plist has an explicit EnvironmentVariables
dict, so launchctl setenv is ignored. Use PlistBuddy to add/set vars
directly in the plist, then unload/load the service to pick them up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Running launchctl unload from inside computer-server kills it before the
load step runs. Use lume ssh from the host instead — it runs outside the
VM so the unload+load sequence completes successfully.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
launchctl unload/load doesn't work correctly for Aqua session agents
from non-GUI SSH sessions. Use bootout/bootstrap which properly handles
the GUI session context (gui/<uid>).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
macOS VMs need 'echo lume | sudo -S' for passwordless sudo. Apply this
to mkdir/mv in copy layers and to /etc/environment writes in env layers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of pulling (decompressing ~30GB) for every ephemeral VM, pull
once into a stopped golden base VM and then APFS-clone it for each use.
Clone is instant via clonefile(2); subsequent test runs skip the ~155s
decompression step entirely.

Also adds CheckpointInfo dataclass and checkpoint/fork/ensure_base
primitives to the Runtime base class, forming the foundation for a
user-facing snapshot/fork API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The custom NWConnection server read with minimumIncompleteLength=1,
causing 'Invalid request body' whenever TCP delivered the POST body
in a second segment (common after several connections).

Replace with ServerBootstrap + NIOHTTP1 pipeline, which handles
Content-Length / chunked reassembly correctly before dispatching
to route handlers. No more spurious 400s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bridge ChannelHandlerContext back via EventLoopPromise so it never
  crosses actor boundaries (fixes 'sending ctx risks data races')
- Remove @mainactor from Server (handlers create LumeController locally)
- Bump swift-sdk from 0.10.0 to 0.12.0 which fixes the data race in
  NetworkTransport that broke clean builds under Swift 6.3

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- In _apply_layers, write image._env to /data/local/tmp/.cua_env on the
  device so env vars survive across independent adb shell invocations
- Prefix every adb transport shell command with `. /data/local/tmp/.cua_env 2>/dev/null`
- Update test_apk_install to use F-Droid APK URL instead of skipping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix env var quoting by writing to a local tempfile and pushing via
  `adb push` instead of shell string escaping
- test_pwa_install now uses android-example-gym-pwa-app manifest URL
  instead of requiring ANDROID_TEST_PWA_URL env var

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tall

- Source .cua_env in GRPCEmulatorTransport shell commands (this is the
  actual transport used by ephemeral Android sandboxes, not ADBTransport)
- Augment PATH with /opt/homebrew/bin so node/npm are found when the
  process is launched without a login shell
- test_pwa_install downloads keystore from android-example-gym-pwa-app repo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add docs/content/docs/cua/guide/sandbox/snapshots.mdx covering the
  snapshot API, use cases, and performance characteristics
- Fix gen_interface_docs.py: single-brace import, escape {}<> outside
  code spans with HTML entities instead of backslashes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d7744265-08dc-4988-8df2-1e605eb5755e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces snapshot support for Cua sandboxes, allowing users to capture running sandbox states as reusable images. It integrates SwiftNIO into the Lume HTTP server, extends the Python SDK with runtime compatibility detection and multi-OS layer execution support, and implements new checkpoint/fork runtime APIs for managing snapshots.

Changes

Cohort / File(s) Summary
Documentation - Snapshots Guide
docs/content/docs/cua/guide/sandbox/meta.json, docs/content/docs/cua/guide/sandbox/snapshots.mdx
Added new snapshots guide page with examples of capturing sandbox state as reusable images, demonstrating use-cases like pre-baked environments and state isolation, including performance comparisons.
Documentation - Interface Reference Regeneration
docs/content/docs/cua/reference/sandbox-sdk/interfaces.mdx
Completely regenerated interface reference documentation with auto-generated format. Updated method signatures across all interfaces: Shell (timeout default 30s), Mouse (scroll/drag refactored), Keyboard (press→keypress), Screen (format/quality params), Clipboard (read/write→get/set), Tunnel (forward returns context), Terminal (new create/send_input/resize/close), Window (simplified), Mobile (comprehensive gesture/input refactor).
Swift - SwiftNIO Server Migration
libs/lume/src/Server/Server.swift, libs/lume/src/Server/HTTP.swift
Replaced Network framework with SwiftNIO stack: migrated from NWListener/NWConnection to ServerBootstrap, added HTTPChannelHandler for HTTP/1.1 parsing and response handling, added HTTPRequest memberwise initializer, changed concurrency model from @MainActor to @unchecked Sendable, improved port-in-use error detection via EADDRINUSE.
Swift - Package Dependencies
libs/lume/Package.swift, libs/lume/Package.resolved
Updated swift-sdk dependency from 0.10.0→0.12.0, added NIOHTTP1 product to lume target, regenerated Package.resolved with updated swift-sdk revision and version.
Python - Runtime Compatibility System
libs/python/cua-sandbox/cua_sandbox/runtime/compat.py
Added new RuntimeSupport dataclass and check_local_support() function to detect host environment capabilities (Docker, QEMU, KVM, HVF, Lume, Android SDK, Java) and determine if images can run locally. Includes skip_if_unsupported() pytest utility for conditional test execution.
Python - Runtime Base Classes
libs/python/cua-sandbox/cua_sandbox/runtime/base.py
Extended Runtime ABC with new checkpoint/fork APIs: ensure_base(), fork(), checkpoint(), list_checkpoints(), delete_checkpoint(). Added CheckpointInfo dataclass for metadata.
Python - LumeRuntime Snapshots
libs/python/cua-sandbox/cua_sandbox/runtime/lume.py
Implemented checkpoint/fork operations in LumeRuntime: ensure_base() and fork() for base VM management, checkpoint() for creating snapshots, list/delete checkpoint support. Refactored start() to prefer running VM fast-path, added _apply_image_layers() for environment/file/layer application post-clone.
Python - DockerRuntime Enhancements
libs/python/cua-sandbox/cua_sandbox/runtime/docker.py
Extended Docker container setup: propagate Image._env and _ports to containers, add LayerExecutor post-container setup for file copying and layer execution, added _docker_bin() resolver for cross-platform Docker binary discovery.
Python - Multi-OS Layer Execution
libs/python/cua-sandbox/cua_sandbox/builder/executor.py
Expanded LayerExecutor for multi-OS support (Linux, macOS, Windows, Android): added os_type parameter, rewrote run_command request format, added write_file() for file transfer, implemented OS-specific command wrapping (sudo on Linux/macOS, pass-through on Android, no-op on Windows). Extended handlers: apt/brew/choco/winget/uv/pip install with timeouts and platform-specific logic, apk_install and pwa_install handlers, proper layer failure detection.
Python - Image & Sandbox Snapshots
libs/python/cua-sandbox/cua_sandbox/image.py, libs/python/cua-sandbox/cua_sandbox/sandbox.py
Added Image._snapshot_source field to track snapshot origin, Image.local_support() to query compatibility, Sandbox.snapshot() to capture running state and return new Image. Updated Sandbox.destroy() to call runtime.delete() for ephemeral VMs when available.
Python - Cloud Transport Snapshots
libs/python/cua-sandbox/cua_sandbox/transport/cloud.py
Added CloudTransport.create_snapshot() POST handler with 600s timeout, _wait_for_snapshot_ready() (no-op, kept for compat), extended _create_vm() to fork from snapshot via source="snapshot" when Image has _snapshot_source. Added endpoint resolution retry loop for local dev mode.
Python - Transport Enhancements
libs/python/cua-sandbox/cua_sandbox/transport/adb.py, libs/python/cua-sandbox/cua_sandbox/transport/grpc_emulator.py, libs/python/cua-sandbox/cua_sandbox/transport/local.py, libs/python/cua-sandbox/cua_sandbox/interfaces/mouse.py, libs/python/cua-sandbox/cua_sandbox/interfaces/clipboard.py
Updated command execution to source /data/local/tmp/.cua_env when available. Mouse.drag() now uses path field format. Clipboard.get() handles dict responses. Local transport supports alternate drag input shape via path parameter.
Python - Android Emulator Runtime
libs/python/cua-sandbox/cua_sandbox/runtime/android_emulator.py
Added IMAGE env var propagation via shell script push, augmented PATH for Node/bubblewrap discovery, improved package_name sanitization (replace non-alphanumeric, prefix digit-starting labels), updated environment composition to use augmented PATH.
Python - Public API Exports
libs/python/cua-sandbox/cua_sandbox/__init__.py, libs/python/cua/cua/__init__.py, libs/python/cua/pyproject.toml
Exported RuntimeSupport, check_local_support, skip_if_unsupported from cua_sandbox. Expanded cua package exports to include all runtime classes and interface types via lazy getattr dispatch. Added local filesystem paths for cua-sandbox, cua-agent, cua-cli dependencies in cua pyproject.
Python - Configuration
libs/python/cua-sandbox/cua_sandbox/_config.py
Updated get_base_url() to check CUA_BASE_URL environment variable before falling back to _global_config.base_url.
Infrastructure - Documentation Generation
scripts/gen_interface_docs.py
Added new script to auto-generate interfaces reference documentation by introspecting interface class definitions, methods, and docstrings from AST, supporting --dry-run mode.
Testing - Configuration & Modules
tests/pytest.ini, libs/python/cua-sandbox/tests/test_snapshots.py, tests/test_image_builder.py, tests/test_interfaces.py, tests/test_runtime.py, libs/python/cua-sandbox/tests/test_android_multitouch.py
Updated pytest pythonpath to discover cua packages. Added comprehensive snapshot integration tests (Linux/Android workflows), image builder tests covering apt/pip/uv/brew/choco/winget/apk/pwa, extensive interface tests for all sandbox methods across guest OS types. Added runtime compatibility gating with skip_if_unsupported(). Removed explicit name overrides from cloud ephemeral tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant NIO as NIO<br/>ServerBootstrap
    participant Handler as HTTPChannel<br/>Handler
    participant Router as Route<br/>Handler
    participant App as Application<br/>Logic

    Client->>NIO: TCP connect
    NIO->>Handler: channel active
    Client->>Handler: send HTTP request bytes
    Handler->>Handler: decode with NIOHTTP1
    Handler->>Handler: accumulate body
    Handler->>Router: handleRequest(HTTPRequest)
    Router->>App: route to handler (async)
    App->>App: execute handler
    App-->>Router: response result
    Router->>Handler: write response
    Handler->>Client: HTTP response bytes
Loading
sequenceDiagram
    participant User
    participant Sandbox as Sandbox
    participant Transport as Cloud<br/>Transport
    participant API as Cloud API
    participant Runtime as Runtime
    participant NewImage as Image

    User->>Sandbox: snapshot(name, stateful)
    Sandbox->>Transport: create_snapshot(name, stateful)
    Transport->>API: POST /v1/vms/{vm}/snapshot
    API-->>Transport: image descriptor
    Transport-->>Sandbox: image dict
    Sandbox->>NewImage: Image(_snapshot_source=descriptor, ...)
    NewImage-->>User: Image object

    User->>Sandbox: Sandbox.create(image=NewImage)
    Sandbox->>Transport: _create_vm(source="snapshot", ...)
    Transport->>API: POST /v1/vms (fork from snapshot)
    API-->>Transport: new VM details
    Transport->>Runtime: apply layers to new VM
    Runtime-->>Sandbox: running sandbox (reusing installed state)
    Sandbox-->>User: Sandbox instance
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

release:lume, feature:snapshots, docs:regenerated, python-sdk:runtime, multi-os

Poem

🐰 A snapshot captures dreams in time,
Where forked sandboxes align,
SwiftNIO flows through Lume's veins bright,
From cloud to host, all runtimes right!
Install once, fork forever free—
The snapshot magic makes it be! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: SDK integration tests, auto-generated docs, and snapshot support, matching the primary objectives of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sdk-tests-and-docs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel

vercel Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Mar 30, 2026 8:03pm

Request Review

…d eventLoopGroup cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Publishable packages changed

  • lume

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@sentry

sentry Bot commented Mar 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
libs/python/cua-sandbox/cua_sandbox/interfaces/mouse.py (1)

35-42: ⚠️ Potential issue | 🔴 Critical

QMPTransport._drag does not support the new path payload format and will fail at runtime.

The Mouse.drag() method sends path=[[start_x, start_y], [end_x, end_y]], but QMPTransport._drag (line 298) expects separate start_x, start_y, end_x, end_y parameters. When send("drag", path=..., button=...) dispatches to _drag, the required parameters will be missing, causing a TypeError.

LocalTransport handles both formats correctly (checks for "path" in params and extracts coordinates), but QMPTransport._drag has no such conversion logic. Update _drag to accept and unpack the path parameter:

async def _drag(
    self, path: list | None = None, start_x: int | None = None, start_y: int | None = None, 
    end_x: int | None = None, end_y: int | None = None, button: str = "left", **_: Any
) -> None:
    if path:
        start_x, start_y = path[0]
        end_x, end_y = path[-1]
    # ... rest of implementation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/interfaces/mouse.py` around lines 35 -
42, QMPTransport._drag currently expects start_x/start_y/end_x/end_y and will
fail when Mouse.drag sends path=[[...],[...]]; update the _drag method signature
to accept a path parameter (and optional start_x/start_y/end_x/end_y) and, at
the start of _drag, if path is provided unpack start and end coordinates from
path[0] and path[-1] respectively before continuing with the existing
implementation; ensure the method still accepts button and ignores extra kwargs
so it remains compatible with send dispatching.
libs/python/cua-sandbox/cua_sandbox/runtime/android_emulator.py (1)

503-510: ⚠️ Potential issue | 🟠 Major

Pass augmented PATH to both npm subprocess and post-install lookup.

Line 503's subprocess.run() lacks env=env, so the npm process inherits the original PATH instead of the augmented one. Line 510's shutil.which("bubblewrap") also omits the path parameter, causing it to search the original PATH. On stripped-PATH hosts, this inconsistency allows the install to succeed while the subsequent lookup fails.

Suggested fix
             subprocess.run(
                 [npm, "install", "-g", "@bubblewrap/cli"],
                 check=True,
                 capture_output=True,
                 timeout=300,
+                env=env,
             )
-            bw = shutil.which("bubblewrap")
+            bw = shutil.which("bubblewrap", path=env["PATH"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/runtime/android_emulator.py` around lines
503 - 510, The npm install subprocess call and the subsequent bubblewrap lookup
are using the original PATH instead of the augmented env; update the
subprocess.run invocation that installs "@bubblewrap/cli" to include env=env
(the same env you build/augment) and change the shutil.which("bubblewrap") call
to pass the augmented path (e.g., shutil.which("bubblewrap", path=env["PATH"])
or env.get("PATH")) so both the install and the post-install lookup use the same
modified PATH.
🟠 Major comments (18)
libs/lume/src/Server/Server.swift-126-126 (1)

126-126: ⚠️ Potential issue | 🟠 Major

@unchecked Sendable requires thread-safe access to mutable state.

Server is marked @unchecked Sendable but serverChannel is accessed without synchronization—written in start() (line 427) and read in stop() (line 440). If stop() is called from a different thread/task before start() assigns the channel, this creates a data race.

🔒 Option 1: Use a lock for serverChannel access
+import NIOConcurrencyHelpers

 final class Server: `@unchecked` Sendable {
     // ...
-    private var serverChannel: (any Channel)?
+    private let serverChannelLock = NIOLock()
+    private var _serverChannel: (any Channel)?
+    private var serverChannel: (any Channel)? {
+        get { serverChannelLock.withLock { _serverChannel } }
+        set { serverChannelLock.withLock { _serverChannel = newValue } }
+    }
🔒 Option 2: Document single-threaded usage contract

If stop() is guaranteed to only be called after start() returns or from the same isolation context, add a doc comment clarifying this constraint.

Also applies to: 171-171

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/lume/src/Server/Server.swift` at line 126, Server is marked `@unchecked`
Sendable but accesses to mutable serverChannel in start() and stop() are
unsynchronized, risking a data race; add a synchronization primitive (e.g., a
private NSLock or DispatchQueue property like serverChannelLock) to the Server
class and wrap all reads/writes of serverChannel in lock() / unlock() (or sync
on the queue) inside start() and stop(); alternatively, if you choose the
single-threaded contract, replace `@unchecked` Sendable with a doc comment on
Server documenting that stop() must only be called after start() returns (or
from the same isolation) and ensure callers abide by that contract.
libs/lume/Package.swift-17-17 (1)

17-17: ⚠️ Potential issue | 🟠 Major

Address breaking changes in swift-sdk 0.12.0.

The version bump from 0.10.0 to 0.12.0 includes breaking API changes aligned with the updated MCP specification. Review the impact of:

  • Tool content representation changes (Tool.Content associated values)
  • Resource field renamed from metadata to size
  • Prompt and completion context argument typing changed from Value to String
  • Elicitation requestedSchema is now required

Verify current MCP product usage of these types is compatible with the changes before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/lume/Package.swift` at line 17, The package update to swift-sdk 0.12.0
introduces breaking API changes: update all usages of Tool.Content (adjust
associated-value patterns/constructors to the new representation), rename any
Resource.metadata accesses/fields to Resource.size, change prompt and completion
context argument types from generic Value to String (update signatures and call
sites for types like Prompt.init / CompletionContext or methods that accept
context), and ensure every Elicitation instance supplies the now-required
requestedSchema field; search for symbols Tool.Content, Resource.metadata,
Prompt/Completion context parameters, and Elicitation.requestedSchema to locate
and update each affected site accordingly.
libs/python/cua-sandbox/cua_sandbox/image.py-123-123 (1)

123-123: ⚠️ Potential issue | 🟠 Major

Preserve _snapshot_source when cloning an Image.

Line 123 adds the snapshot descriptor, but _add_layer() and _with() do not copy it forward. Any snapshot_image.run(...), .env(...), .copy(...), or .expose(...) call silently drops the snapshot source, so CloudTransport stops treating the result as a forkable snapshot image.

Suggested fix
 def _add_layer(self, layer: Dict[str, Any]) -> Image:
     return Image(
         os_type=self.os_type,
         distro=self.distro,
         version=self.version,
         kind=self.kind,
         _layers=self._layers + (layer,),
         _env=self._env,
         _ports=self._ports,
         _files=self._files,
         _registry=self._registry,
         _disk_path=self._disk_path,
         _agent_type=self._agent_type,
+        _snapshot_source=self._snapshot_source,
     )

 def _with(self, **kwargs) -> Image:
     """Return a new Image with specific fields overridden."""
     fields = {
         "os_type": self.os_type,
         "distro": self.distro,
         "version": self.version,
         "kind": self.kind,
         "_layers": self._layers,
         "_env": self._env,
         "_ports": self._ports,
         "_files": self._files,
         "_registry": self._registry,
         "_disk_path": self._disk_path,
         "_agent_type": self._agent_type,
+        "_snapshot_source": self._snapshot_source,
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/image.py` at line 123, The
_snapshot_source attribute added to Image is not preserved when creating
modified Image instances, so methods that build on an image (notably _add_layer
and _with, which back run, env, copy, expose) drop _snapshot_source causing
CloudTransport to lose snapshot semantics; update _add_layer() and _with() to
copy the original instance’s _snapshot_source into the new Image before
returning (i.e., if self._snapshot_source is set, set new_image._snapshot_source
= self._snapshot_source) so all chainable methods (run, env, copy, expose)
created via those helpers retain the snapshot descriptor.
libs/python/cua-sandbox/cua_sandbox/sandbox.py-297-301 (1)

297-301: ⚠️ Potential issue | 🟠 Major

Populate os_type from OS metadata, not from kind.

Line 298 currently turns descriptors like {"kind": "vm"} into Image(os_type="vm"). That misclassifies the snapshot image for OS-gated builder methods and compatibility checks, and the current constructor also drops kind entirely when _transport._image is missing.

Suggested fix
         return ImageCls(
-            os_type=image_desc.get("kind", src_image.os_type if src_image else "linux"),
-            distro=src_image.distro if src_image else "ubuntu",
-            version=src_image.version if src_image else "24.04",
+            os_type=(
+                image_desc.get("os_type")
+                or image_desc.get("os")
+                or (src_image.os_type if src_image else "linux")
+            ),
+            distro=image_desc.get("distro", src_image.distro if src_image else "ubuntu"),
+            version=image_desc.get("version", src_image.version if src_image else "24.04"),
+            kind=image_desc.get("kind", src_image.kind if src_image else "vm"),
             _snapshot_source=image_desc,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/sandbox.py` around lines 297 - 301, The
code is using image_desc.get("kind") to populate ImageCls.os_type which
misclassifies images; change the os_type assignment to read OS metadata from the
descriptor (e.g. image_desc.get("os", {}).get("type") or
image_desc.get("os_type")) and only fallback to src_image.os_type or "linux" if
that metadata is absent, leaving image_desc unchanged for _snapshot_source;
update the ImageCls(...) call so os_type= image_desc.get("os", {}).get("type",
image_desc.get("os_type", src_image.os_type if src_image else "linux")) instead
of using image_desc.get("kind").
libs/python/cua-sandbox/cua_sandbox/runtime/docker.py-198-225 (1)

198-225: ⚠️ Potential issue | 🟠 Major

Tear the container down if post-start provisioning fails.

Lines 198-225 add several new failure points after docker run succeeds. If env propagation, file copy, or a build layer errors, start() raises before a Sandbox exists, and the half-provisioned container is left running.

Suggested fix
         await self.is_ready(info)

-        # Apply image layers and files via computer-server
-        env_items = getattr(image, "_env", ())
-        file_items = getattr(image, "_files", ())
-        has_work = image._layers or file_items
-        if has_work or env_items:
-            from cua_sandbox.builder.executor import LayerExecutor
-
-            executor = LayerExecutor(f"http://{info.host}:{info.api_port}", os_type=image.os_type)
-
-            # Write env vars to a sourceable profile script so run layers can access them
-            if env_items and image.os_type != "windows":
-                ...
-
-            # Apply files before layers so later run layers can reference copied files
-            for src, dst in file_items:
-                await executor.execute_layers([{"type": "copy", "src": src, "dst": dst}])
-
-            if image._layers:
-                await executor.execute_layers(list(image._layers))
+        try:
+            # Apply image layers and files via computer-server
+            env_items = getattr(image, "_env", ())
+            file_items = getattr(image, "_files", ())
+            has_work = image._layers or file_items
+            if has_work or env_items:
+                from cua_sandbox.builder.executor import LayerExecutor
+
+                executor = LayerExecutor(
+                    f"http://{info.host}:{info.api_port}",
+                    os_type=image.os_type,
+                )
+
+                # Write env vars to a sourceable profile script so run layers can access them
+                if env_items and image.os_type != "windows":
+                    ...
+
+                for src, dst in file_items:
+                    await executor.execute_layers([{"type": "copy", "src": src, "dst": dst}])
+
+                if image._layers:
+                    await executor.execute_layers(list(image._layers))
+        except Exception:
+            subprocess.run([docker, "rm", "-f", name], capture_output=True)
+            raise
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/runtime/docker.py` around lines 198 -
225, The new provisioning steps (env/file/layers) in start() can fail after
docker run succeeds, leaving a half-provisioned container running; wrap the
block that creates LayerExecutor and runs env/file/layer work in a try/except
and on any exception ensure the just-started container is torn down before
re-raising. Specifically, around the code that instantiates LayerExecutor and
uses executor.run_command/execute_layers, catch exceptions and call the existing
cleanup routine used elsewhere (e.g., the Sandbox teardown/stop method or the
internal container removal helper—refer to the instance field that stores the
started container like self.container_id or self._container and call the
matching cleanup method such as
self.teardown()/self.stop()/self._remove_container(...) before raising), then
re-raise the error.
libs/python/cua-sandbox/tests/test_android_multitouch.py-146-146 (1)

146-146: ⚠️ Potential issue | 🟠 Major

Move the compatibility gate out of the session fixture.

Line 146 executes when pytest instantiates local_android_sb, which happens because _reset_between_tests (autouse=True) has it as a direct parameter. On hosts without local Android support, this causes the entire test session to skip—including cloud tests that have their own API-key gate and don't depend on local emulator support.

Move skip_if_unsupported(Image.android(str(_API_LEVEL))) to the sb fixture in TestAndroidMultitouchLocal only, or refactor _reset_between_tests to request local_android_sb lazily (e.g., via request.getfixturevalue("local_android_sb")) so it's only resolved when actually needed by local tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/tests/test_android_multitouch.py` at line 146, The
compatibility gate skip_if_unsupported(Image.android(str(_API_LEVEL))) is being
evaluated during pytest fixture instantiation (because _reset_between_tests
directly depends on local_android_sb), causing the whole session to skip on
hosts without local Android; fix by moving that call out of the session-level
fixture: either add skip_if_unsupported(Image.android(str(_API_LEVEL))) inside
the sb fixture of TestAndroidMultitouchLocal (so it only runs for local tests)
or change _reset_between_tests to obtain local_android_sb lazily (use
request.getfixturevalue("local_android_sb") inside the fixture body) so
local_android_sb is only resolved when a test actually needs it. Ensure
references to skip_if_unsupported, Image.android, local_android_sb,
_reset_between_tests, and the sb fixture are updated accordingly.
libs/python/cua-sandbox/cua_sandbox/runtime/docker.py-207-218 (1)

207-218: ⚠️ Potential issue | 🟠 Major

Use shlex.quote() to safely emit environment variables to /etc/profile.d/cua-env.sh.

Lines 213–217 only escape single quotes in double-quoted strings. Values containing $(), $VAR, backticks, or newlines will be re-evaluated or mangled when the script is sourced. Use shlex.quote() to properly escape arbitrary strings, and validate variable names with re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", k) to prevent injection.

Note: The same vulnerability exists in libs/python/cua-sandbox/cua_sandbox/runtime/lume.py at line 403.

Suggested fix
+                import re
+                import shlex
                 for k, v in env_items:
-                    safe_v = v.replace("'", "'\\''")
+                    if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", k):
+                        raise ValueError(f"Invalid environment variable name: {k!r}")
+                    line = f"export {k}={shlex.quote(v)}"
                     await executor.run_command(
-                        f"printf 'export {k}=\"{safe_v}\"\\n' "
+                        f"printf '%s\\n' {shlex.quote(line)} "
                         f"| sudo tee -a /etc/profile.d/cua-env.sh > /dev/null"
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/runtime/docker.py` around lines 207 -
218, The current code in the block that writes env vars to
/etc/profile.d/cua-env.sh (guarded by image.os_type != "windows" and using
executor.run_command) manually escapes single quotes and is unsafe for values
with $(), backticks, newlines, etc.; replace the manual escaping with Python's
shlex.quote() for the value and ensure the variable name k is validated with
re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", k) before emitting it; keep writing via
executor.run_command to handle sudo/tee but construct the exported line as
export {k}={shlex.quote(v)} (and skip or raise on invalid names) and apply the
same fix in the corresponding block in runtime/lume.py (around the noted line).
libs/python/cua-sandbox/cua_sandbox/builder/executor.py-210-214 (1)

210-214: ⚠️ Potential issue | 🟠 Major

Quote privileged copy destinations.

Lines 213-214 splice dst_dir and dst directly into mkdir/mv. A destination like /Applications/My App/bin will break, and shell metacharacters in the path become command injection. _sh_quote() is already in this file; use it for both arguments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/builder/executor.py` around lines 210 -
214, The current commands build shell strings by splicing dst_dir, dst and
tmp_path directly which breaks on spaces and allows injection; update the block
in executor.py to use the existing _sh_quote() helper for any user-supplied path
when calling run_command: quote dst_dir for the mkdir -p invocation and quote
both tmp_path and dst for the mv invocation (keep using the computed sudo
variable and the run_command method). Ensure you still guard the mkdir call with
the dst_dir and "/" checks, but pass the quoted path to run_command rather than
the raw variables.
tests/test_interfaces.py-52-63 (1)

52-63: ⚠️ Potential issue | 🟠 Major

The tunnel helper is POSIX-only, but Windows reuses it.

python3 -c "... &" assumes a POSIX shell, background operator, and a python3 binary in the guest. TestTunnelWindows calls this helper unchanged, so those tests can fail before the tunnel code is exercised. Pick the bootstrap command per guest OS, or provision Python explicitly in the non-Linux images.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_interfaces.py` around lines 52 - 63, The helper _start_http_server
currently invokes a POSIX-specific bootstrap (uses "python3 -c ... &") which
breaks Windows tests like TestTunnelWindows; update _start_http_server to choose
the bootstrap command based on the guest OS or provide an explicit
Windows-compatible command when called from TestTunnelWindows: detect the guest
OS from the sandbox API or add a parameter (e.g., shell_type or is_windows) to
_start_http_server, and when Windows is detected use a PowerShell/start-process
or cmd /C start /B variant (or install/provision python in the Windows image)
instead of the POSIX background "&" and "python3" binary; update the
TestTunnelWindows invocation to pass the Windows flag if you add a parameter so
the correct boot command is used.
libs/python/cua-sandbox/cua_sandbox/transport/cloud.py-94-106 (1)

94-106: ⚠️ Potential issue | 🟠 Major

Don't block on normal .cua.sh endpoints.

Line 99 treats every .cua.sh URL as “not ready yet”, but _resolve_endpoint() already returns .cua.sh for the normal hosted reverse-proxy path. On prod this can add the full 120s wait before HTTPTransport even tries to connect. Gate this loop behind a local-dev base_url/host check instead of the endpoint suffix itself.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/transport/cloud.py` around lines 94 -
106, The loop currently treats any .cua.sh endpoint as “not ready” and polls for
120s; change it to only poll when running against a local-dev host by guarding
the loop with a local-dev base_url/host check (e.g., check self.base_url or
self._base_url for localhost/127.0.0.1 or your dev domain) and otherwise skip
the polling so production .cua.sh endpoints are not delayed; keep using
_resolve_endpoint(vm_info), _get_vm(self._name) and let _wait_for_server_ready
handle errors as before.
libs/python/cua-sandbox/cua_sandbox/builder/executor.py-174-185 (1)

174-185: ⚠️ Potential issue | 🟠 Major

Don't interpolate raw env text into /etc/environment.

Line 184 uses an unquoted heredoc around raw k/v strings. Quotes, $VAR, backticks, or newlines in user input will either expand during the shell write or leave /etc/environment malformed. Validate the key and escape the persisted content before writing it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/builder/executor.py` around lines 174 -
185, _in _exec_env_: do not interpolate raw keys/values into an unquoted
heredoc; instead validate each env key (e.g. enforce pattern like
^[A-Z_][A-Z0-9_]*$), reject or fail on invalid keys, and sanitize/escape values
(e.g. replace or fail on raw newlines, and escape quotes/backslashes) before
persisting. When writing to /etc/environment use a non-expanding heredoc
(<<'EOF') or write with a safe printf/append approach run under sudo to avoid
shell expansion; update the construction around variables, lines and cmd in
_exec_env to validate keys, escape values, and use a quoted heredoc or printf to
safely append KEY="escaped_value" entries.
libs/python/cua-sandbox/tests/test_snapshots.py-31-66 (1)

31-66: ⚠️ Potential issue | 🟠 Major

Measure “create+install” after the install finishes.

Line 34 captures t_create immediately after boot, so Line 64 compares fork time against raw VM startup even though the assertion message says “create+install”. That makes the test fail for the wrong reason when fork is slower than boot but still faster than boot+package install.

Suggested fix
     async with Sandbox.ephemeral(Image.linux("ubuntu", "24.04")) as sb:
-        t_create = time.monotonic() - t_create_start
-
         # Install something unique
         result = await sb.shell.run(
             "apt-get update -qq && apt-get install -y -qq cowsay", timeout=120
         )
         assert result.success, f"Install failed: {result.stderr}"
@@
         result = await sb.shell.run("/usr/games/cowsay hello")
         assert result.success
         assert "hello" in result.stdout
+        t_create = time.monotonic() - t_create_start
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/tests/test_snapshots.py` around lines 31 - 66, The
test measures t_create too early; move the t_create = time.monotonic() -
t_create_start assignment to after the install and verification steps so
t_create reflects "create+install" instead of just VM startup. Specifically, in
the Sandbox.ephemeral block that uses t_create_start, set t_create after the
sb.shell.run calls and the assertions that verify cowsay (i.e., after the
install result/asserts and the "/usr/games/cowsay hello" check), before calling
await sb.snapshot(...), so the later comparison with t_fork correctly compares
fork time to create+install time.
libs/python/cua-sandbox/cua_sandbox/runtime/compat.py-419-437 (1)

419-437: ⚠️ Potential issue | 🟠 Major

Mark Android unsupported when Java is missing.

When java_ok is false, Line 434 still returns supported=True. skip_if_unsupported(Image.android()) therefore won't skip, but the emulator start path immediately fails in _java_env(). supported should stay false unless the host can actually boot or auto-install the remaining pieces.

Suggested fix
         return RuntimeSupport(
-            supported=True,  # SDK auto-installs on macOS/Linux if Java is present
+            supported=java_ok,
             hw_accel=hw,
             runtime_installed=installed,
             auto_installable=auto and java_ok,
             runtime_name="android_emulator",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/runtime/compat.py` around lines 419 -
437, The code currently returns RuntimeSupport(supported=True) even when java_ok
is False; update the supported flag to reflect actual boot capability by passing
supported=java_ok (so Android is marked unsupported when Java is missing),
keeping the existing hw_accel, runtime_installed, and auto_installable logic
(references: java_ok, sdk_ok, installed, auto, RuntimeSupport).
libs/python/cua/cua/__init__.py-46-55 (1)

46-55: ⚠️ Potential issue | 🟠 Major

Don't export compat helpers as None.

If cua_sandbox.runtime.compat is absent, from cua import skip_if_unsupported still succeeds because the name exists in the module and __all__, but callers only discover the problem when None is invoked. Either omit these exports in the fallback path or replace them with stubs that raise ImportError.

Also applies to: 119-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua/cua/__init__.py` around lines 46 - 55, The module currently
assigns RuntimeSupport, check_local_support, and skip_if_unsupported to None
when cua_sandbox.runtime.compat is missing, causing silent import success;
instead, change the fallback so these names are not exported (remove them from
__all__) or replace them with stubs that immediately raise ImportError when
called/instantiated; update the same treatment for the additional compatibility
names referenced later (the other symbols you set to None around the later
block), and ensure __all__ only lists names that are actually usable so
importing from the package fails fast with a clear ImportError.
libs/python/cua-sandbox/cua_sandbox/runtime/lume.py-188-216 (1)

188-216: ⚠️ Potential issue | 🟠 Major

Checkpoint listing is too broad.

checkpoint() lets callers choose any checkpoint_name, so status == "suspended" is not enough to identify checkpoints. As written, list_checkpoints() will surface ordinary stopped sandboxes, and delete_checkpoint() can then delete them.

libs/python/cua-sandbox/cua_sandbox/runtime/lume.py-372-377 (1)

372-377: ⚠️ Potential issue | 🟠 Major

Treat env values as literals, not shell fragments.

Both branches interpolate k and v directly into shell/PlistBuddy commands. Even benign values like $HOME or foo bar stop being literals here; worse, command substitutions/newlines can write broken config or execute unintended shell when /etc/profile.d/cua-env.sh is later sourced by libs/python/cua-sandbox/cua_sandbox/builder/executor.py:109-125. Validate variable names and emit properly quoted literals instead of composing shell with raw strings.

Also applies to: 402-406

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/runtime/lume.py` around lines 372 - 377,
The loop that builds and passes PlistBuddy commands currently interpolates env
key/value directly into a shell string (see env_items loop and
executor.run_command calls), which allows shell/meta characters to be
interpreted; validate keys with a strict regex like r'^[A-Za-z_][A-Za-z0-9_]*$'
and reject or sanitize invalid names, and stop composing raw shell fragments for
values—emit properly quoted literals (e.g., use shlex.quote on keys/values if
executor.run_command must receive a shell string) or, better, call PlistBuddy
via a safe API (pass argv lists to subprocess/ executor.run_command if it
supports lists) or write the EnvironmentVariables dict using plistlib to avoid
shell entirely; apply the same fixes to the similar block at lines 402-406.
libs/python/cua-sandbox/cua_sandbox/runtime/lume.py-149-154 (1)

149-154: ⚠️ Potential issue | 🟠 Major

Require the cached base VM to be stopped.

The docstring says the base cache is a stopped golden VM, but this branch also accepts running. The next step clones from that base, so reusing a live base can produce dirty clones or clone failures.

Possible fix
         if resp.status_code == 200:
             vm = resp.json()
-            if vm.get("status") in ("stopped", "running"):
+            if vm.get("status") == "stopped":
                 logger.info(f"Base VM '{base_name}' already exists — skipping pull")
                 return CheckpointInfo(name=base_name, runtime_type="lume", created_at=time.time())
+            if vm.get("status") == "running":
+                raise RuntimeError(
+                    f"Base VM '{base_name}' is running; stop it before cloning"
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/runtime/lume.py` around lines 149 - 154,
The branch that treats an existing base VM as usable should only accept a
stopped golden VM, not "running": change the condition that currently checks
vm.get("status") in ("stopped", "running") to require equality to "stopped" so
only stopped bases are reused; update the log/message to reflect that we are
skipping pull because the base VM is stopped and return the same CheckpointInfo
(references: client.get(...)/resp, vm.get("status"), base_name, and
CheckpointInfo).
libs/python/cua-sandbox/cua_sandbox/runtime/lume.py-86-109 (1)

86-109: ⚠️ Potential issue | 🟠 Major

Handle existing stopped VMs before cloning a new one.

Only running is special-cased here. If /lume/vms/{name} already exists in a stopped state, this falls through to fork(base_name, name), which collides with the existing VM name instead of resuming or intentionally recreating it.

Possible fix
         if vm.get("status") == "running":
             logger.info(f"Lume VM {name} already running")
             ip = await self._wait_for_ip(name, lume_url)
             await self._deliver_vnc_config(name, lume_url)
             info = RuntimeInfo(host=ip, api_port=self.api_port, name=name)
             await self.is_ready(info)
             await self._apply_image_layers(image, info)
             return info
+        if vm.get("status") in ("stopped", "stop"):
+            logger.info(f"Lume VM {name} already exists but is stopped; resuming")
+            return await self.resume(image, name, **opts)
 
         oci_ref = image._registry or MACOS_VERSION_IMAGES.get(image.version or "") or MACOS_SEQUOIA
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua-sandbox/cua_sandbox/runtime/lume.py` around lines 86 - 109,
The current fast-path only handles status "running" and ignores an existing VM
in "stopped" state which causes fork(base_name, name) to collide; update the
logic that checks vm = resp.json() so that if vm exists and vm.get("status") ==
"stopped" you treat it like a resume rather than cloning: call
self._run_vm(name, lume_url, opts) to start the stopped VM, then follow the same
post-start steps (await self._wait_for_ip(name, lume_url), await
self._deliver_vnc_config(name, lume_url), construct RuntimeInfo, await
self.is_ready(info), await self._apply_image_layers(image, info), and return
info) instead of calling fork(base_name, name). Ensure you still keep the
existing handling for "running" and the pull/clone path for non-existent VMs
(functions referenced: _wait_for_ip, _deliver_vnc_config, _run_vm, ensure_base,
fork, _apply_image_layers).
🟡 Minor comments (3)
docs/content/docs/cua/guide/sandbox/snapshots.mdx-47-49 (1)

47-49: ⚠️ Potential issue | 🟡 Minor

Clarify that local snapshot support is not a public sb.snapshot() flow yet.

Line 48 says local sandboxes have "limited support", but Sandbox.snapshot() still raises NotImplementedError for non-cloud transports in libs/python/cua-sandbox/cua_sandbox/sandbox.py Lines 288-289. As written, this reads like a supported local user path when the public API still rejects it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/cua/guide/sandbox/snapshots.mdx` around lines 47 - 49, The
doc text incorrectly implies local sandboxes have a usable snapshot flow; update
the copy to clarify that local sandbox transports (Docker, Lume, QEMU) do not
expose a public snapshot API yet because Sandbox.snapshot() currently raises
NotImplementedError for non-cloud transports. Change the Callout to say
snapshots are supported on Cua Cloud sandboxes and local sandbox snapshot
functionality is experimental/internal or not available via the public
Sandbox.snapshot() method for non-cloud transports (referencing
Sandbox.snapshot() in cua_sandbox/sandbox.py), so users should rely on Cua Cloud
for the public snapshot flow.
tests/test_image_builder.py-367-389 (1)

367-389: ⚠️ Potential issue | 🟡 Minor

This test doesn't prove the PWA was installed.

Lines 387-388 only assert that pm list packages executed successfully; that command returns 0 even when the target package is absent. Pass a deterministic package_name to pwa_install() and assert that exact package shows up in stdout so the test actually exercises the install path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_image_builder.py` around lines 367 - 389, The test
test_pwa_install currently only checks that "pm list packages" ran successfully
but not that the PWA was installed; update the Image.android().pwa_install call
to pass a deterministic package_name (e.g., the expected app id) and then run pm
list packages and assert that the package_name appears in the command stdout
(use sb.shell.run(...).stdout or equivalent) instead of only asserting r.success
so the test verifies the installed package. Reference
Image.android().pwa_install, test_pwa_install, Sandbox.ephemeral and
sb.shell.run when making the change.
docs/content/docs/cua/reference/sandbox-sdk/interfaces.mdx-3-3 (1)

3-3: ⚠️ Potential issue | 🟡 Minor

Fix the encoding artifact in the generated description.

The replacement character will show up in page metadata/search snippets and suggests the generator is emitting invalid text. Since this page is generated, please fix the generator output rather than hand-editing the MDX.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/docs/cua/reference/sandbox-sdk/interfaces.mdx` at line 3, The
generated frontmatter description contains a replacement character '�' from the
generator; fix scripts/gen_interface_docs.py so it emits valid UTF-8 text: open
all source files and templates with explicit encoding='utf-8', avoid using
decode/encode with errors='replace' (use 'strict' or 'ignore' where
appropriate), sanitize or normalize any non-ASCII characters (e.g. replace smart
quotes or control chars) before writing, and ensure the description string
written to the MDX frontmatter is the cleaned/normalized Unicode value rather
than a fallback replacement character.
🧹 Nitpick comments (5)
libs/lume/src/Server/Server.swift (3)

47-50: HTTP headers with duplicate names are overwritten.

HTTP allows multiple headers with the same name (e.g., Set-Cookie, Cookie). The current conversion to [String: String] keeps only the last value. Consider whether any routes need access to all header values.

♻️ Alternative: Join duplicate header values
 var headers: [String: String] = [:]
 for (name, value) in head.headers {
-    headers[name.description] = value
+    if let existing = headers[name.description] {
+        headers[name.description] = existing + ", " + value
+    } else {
+        headers[name.description] = value
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/lume/src/Server/Server.swift` around lines 47 - 50, The current
conversion in Server.swift collects request headers into a [String: String]
named headers by iterating head.headers and assigning headers[name.description]
= value, which drops duplicate header names; change this to preserve all values
(either by making headers a [String: [String]] and appending value to
headers[name.description], or by joining duplicates with ", " for a [String:
String] representation) so routes that need multiple values (e.g.,
Set-Cookie/Cookie) can access them; update any downstream consumers of headers
accordingly (look for the headers variable and places that read it to adapt to
the new shape or joined format).

57-63: Duplicate request/response logging.

The request is logged here and again in handleRequest() (line 446). Similarly, the response is logged in writeResponse() (line 100) and handleRequest() (line 460). Consider removing one set to reduce log noise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/lume/src/Server/Server.swift` around lines 57 - 63, The request/response
are being logged twice—once at the top of the request pipeline (the Logger.info
that logs "Received request" with metadata including method/path/body) and again
inside handleRequest(), and likewise responses are logged in writeResponse() and
again in handleRequest(); remove the duplicate logging to reduce noise by
keeping a single canonical log location: either remove the Logger.info call that
logs the request/body (the one outside handleRequest()) or remove the
request/response logs inside handleRequest()/writeResponse() so that only
handleRequest() (or only writeResponse()/the pipeline entry) emits request and
response logs; update or document which functions (handleRequest, writeResponse,
and the earlier Logger.info block) will be responsible for logging and ensure
any necessary metadata (method, path, body, status) is preserved in the retained
log location.

262-268: Consider logging JSON parse failures at debug level.

The empty catch {} silently ignores malformed JSON in the /stop request body. While acceptable for an optional parameter, a debug-level log would aid troubleshooting.

♻️ Add debug logging
-                        } catch {}
+                        } catch {
+                            Logger.debug("Failed to parse /stop body JSON", metadata: ["error": "\(error)"])
+                        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/lume/src/Server/Server.swift` around lines 262 - 268, The empty catch
silently swallows JSON parsing errors for the /stop request; update the catch
after JSONSerialization.jsonObject to log the parse failure at debug level
(include the error and optionally the raw bodyData), referencing the existing
JSONSerialization.jsonObject call and the storage variable so you only log when
parsing fails; use the project's logger API (e.g., logger.debug("Failed to parse
/stop body: \(error)", ...) or equivalent) instead of leaving catch empty.
scripts/gen_interface_docs.py (1)

78-112: Minor: Signature formatting may be incorrect for keyword-only arguments.

When a method has keyword-only arguments but no *args, the generated signature won't include the bare * separator that Python requires to distinguish keyword-only args. For example, def foo(a, *, b=1) would render as (a, b = 1) instead of (a, *, b = 1).

For documentation purposes this is likely acceptable, but if you want strict Python signature accuracy:

♻️ Proposed fix to add bare * separator
     for arg in all_args:
         if arg.arg == "self":
             continue
         parts.append(_format_arg(arg, defaults))
 
+    # Add bare * separator if there are kwonlyargs but no vararg
+    if args.kwonlyargs and not args.vararg:
+        parts.append("*")
+
     for arg in args.kwonlyargs:
         parts.append(_format_arg(arg, defaults))
 
     if args.vararg:
         parts.append(f"*{args.vararg.arg}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/gen_interface_docs.py` around lines 78 - 112, The generated signature
in _method_signature omits the bare '*' when there are keyword-only args but no
vararg; update the logic in _method_signature to insert a standalone "*" into
parts before appending args.kwonlyargs whenever args.kwonlyargs is non-empty and
args.vararg is None (use the existing parts list and args.kwonlyargs/args.vararg
symbols to detect this), then continue formatting kw-only args as before so
signatures like (a, *, b=1) are produced correctly.
libs/python/cua/pyproject.toml (1)

69-73: Consider aligning source resolution with the root workspace configuration.

The root pyproject.toml defines cua-agent with workspace = true, while this file uses path-based references. This creates different resolution behavior depending on whether you install from the workspace root or from libs/python/cua directly.

This works for local development but may cause confusion. Consider adding cua-sandbox and cua-cli to the root workspace members for consistency, or document this intentional divergence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/python/cua/pyproject.toml` around lines 69 - 73, The [tool.uv.sources]
entries in this pyproject.toml (cua-sandbox, cua-agent, cua-cli) create
path-based resolution that diverges from the root workspace's use of workspace =
true for cua-agent; to fix, either add cua-sandbox and cua-cli as members in the
root workspace section of the root pyproject.toml (so all three packages use
workspace resolution consistently) or add a short comment in this
[tool.uv.sources] block explaining the intentional divergence and when path vs
workspace resolution should be used; reference the symbols cua-sandbox,
cua-agent, cua-cli and the workspace = true setting when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f50b674d-5f07-42ea-af8c-c57084be1924

📥 Commits

Reviewing files that changed from the base of the PR and between 91c5665 and 73683d5.

📒 Files selected for processing (34)
  • docs/content/docs/cua/guide/sandbox/meta.json
  • docs/content/docs/cua/guide/sandbox/snapshots.mdx
  • docs/content/docs/cua/reference/sandbox-sdk/interfaces.mdx
  • libs/lume/Package.resolved
  • libs/lume/Package.swift
  • libs/lume/src/Server/HTTP.swift
  • libs/lume/src/Server/Server.swift
  • libs/python/cua-sandbox/cua_sandbox/__init__.py
  • libs/python/cua-sandbox/cua_sandbox/_config.py
  • libs/python/cua-sandbox/cua_sandbox/builder/executor.py
  • libs/python/cua-sandbox/cua_sandbox/image.py
  • libs/python/cua-sandbox/cua_sandbox/interfaces/clipboard.py
  • libs/python/cua-sandbox/cua_sandbox/interfaces/mouse.py
  • libs/python/cua-sandbox/cua_sandbox/runtime/android_emulator.py
  • libs/python/cua-sandbox/cua_sandbox/runtime/base.py
  • libs/python/cua-sandbox/cua_sandbox/runtime/compat.py
  • libs/python/cua-sandbox/cua_sandbox/runtime/docker.py
  • libs/python/cua-sandbox/cua_sandbox/runtime/lume.py
  • libs/python/cua-sandbox/cua_sandbox/sandbox.py
  • libs/python/cua-sandbox/cua_sandbox/transport/adb.py
  • libs/python/cua-sandbox/cua_sandbox/transport/cloud.py
  • libs/python/cua-sandbox/cua_sandbox/transport/grpc_emulator.py
  • libs/python/cua-sandbox/cua_sandbox/transport/local.py
  • libs/python/cua-sandbox/tests/image/__init__.py
  • libs/python/cua-sandbox/tests/interfaces/__init__.py
  • libs/python/cua-sandbox/tests/test_android_multitouch.py
  • libs/python/cua-sandbox/tests/test_runtime.py
  • libs/python/cua-sandbox/tests/test_snapshots.py
  • libs/python/cua/cua/__init__.py
  • libs/python/cua/pyproject.toml
  • scripts/gen_interface_docs.py
  • tests/pytest.ini
  • tests/test_image_builder.py
  • tests/test_interfaces.py
💤 Files with no reviewable changes (1)
  • libs/python/cua-sandbox/tests/test_runtime.py

Comment thread libs/lume/src/Server/Server.swift
Comment thread libs/python/cua-sandbox/cua_sandbox/builder/executor.py Outdated
Comment thread libs/python/cua-sandbox/cua_sandbox/runtime/android_emulator.py
- Add Windows paths to _sdk_path() and check for emulator.exe
- Remove hard unsupported bail on Windows; detect existing SDK + WHPX
- Use sh -c and source /data/local/tmp/.cua_env for Android run layers
- Shell-quote env var values with shlex.quote and validate key names

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Publishable packages changed

  • lume

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

- Add Windows commandlinetools download URL to _ensure_sdk()
- Use .exe/.bat extensions for binary detection on Windows
- Remove Windows-only bail in compat.py — auto-install works everywhere

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Publishable packages changed

  • lume

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

- _find_bin() now checks .exe and .bat extensions on Windows
- grpc_emulator _find_adb() checks for adb.exe on Windows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Publishable packages changed

  • lume

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Publishable packages changed

  • lume

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

…view

Security:
- executor.py, docker.py, lume.py: use shlex.quote() for env var values and
  validate names with re.fullmatch() to prevent shell injection in profile scripts
- executor.py: quote dst/dst_dir with _sh_quote() in privileged copy commands

Correctness:
- image.py: propagate _snapshot_source through _add_layer() and _with() so
  chained image mutations don't silently drop the snapshot descriptor
- sandbox.py: use src_image.os_type/distro/version/kind for snapshot Image
  instead of image_desc["kind"] which is snapshot kind, not OS type
- lume.py: resume existing stopped VMs instead of re-cloning (name collision);
  require base VM to be stopped before cloning; stop→clone→restart in
  checkpoint() to match cloud behaviour; scope list/delete_checkpoint to
  cua-base-* / cua-ckpt-* prefix; fix cloud.py .cua.sh polling loop to only
  run in local-dev mode (not prod reverse-proxy)
- compat.py: return supported=java_ok for Android (Java is hard prerequisite)
- cua/__init__.py: replace None fallbacks with stubs that raise ImportError
- docker.py: tear down container on post-start provisioning failure
- test_snapshots.py: capture t_create after install completes, not after boot

Concurrency:
- Server.swift: protect serverChannel and eventLoopGroup with NSLock to
  eliminate data race between start() writer and stop() reader

Tests:
- test_android_multitouch.py: resolve local_android_sb lazily in autouse
  fixture so cloud tests don't trigger emulator boot or compat skip
- test_interfaces.py: fix _start_http_server docstring and formatting

Style: run isort + black across affected files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Publishable packages changed

  • lume

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@ddupont808 ddupont808 added the release:lume Release lume on merge label Mar 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📦 Publishable packages changed

  • lume — will auto-release on merge

@github-actions

Copy link
Copy Markdown
Contributor

📦 Publishable packages changed

  • lume

Add release:<service> labels to auto-release on merge (+ optional bump:minor or bump:major, default is patch).
Or add no-release to skip.

@ddupont808
ddupont808 merged commit d7a2452 into main Mar 30, 2026
20 of 23 checks passed
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