mybasev1.0.21

Sandboxes

Temporary isolated Node.js + Chromium workspaces for untrusted code, browser automation, file processing, and AI agents.

An optional project service that runs code you don't trust in a throwaway, isolated workspace. Each sandbox is a gVisor container with Node.js, Playwright, and headless Chromium, its own temporary files, and internet access limited to public HTTP(S). Your server sends it code or commands, moves files in and out, drives the browser, and keeps the results as private artifacts.

mybase supplies the compute, lifecycle, authentication, browser connection, and artifact storage. What runs inside is up to your app. No model provider or model key is involved.

What you can use it for

UseHow
Run user-submitted code (playgrounds, coding exercises, grading, plugins, formulas)execute with code and a timeoutMs; read stdout/stderr/exitCode
Browser automation (scraping, form filling, end-to-end checks of a site)Playwright inside the sandbox, or connect your own Playwright over CDP
Screenshots, PDFs, link previews, OG imagesChromium page.screenshot() / page.pdf(), then export or download the file
Process untrusted files (parse uploads, convert or transform data)upload the file, run code on it, download the result
Tools for AI agents (code interpreter, web browsing)Expose execute and exportArtifact as tools in your model's tool loop
Background jobs that need isolationCombine with queues or cron: one sandbox per job

Examples in the repository: examples/sandbox/run-code.ts (run a script, print its output), html-to-pdf.ts (render HTML to a PDF), page-capture.ts (open a page, capture text, responses, images, and a screenshot), and agent-tools.ts (provider-neutral tool definitions for an LLM).

Start here

  1. On the Linux Docker host, run mybase server sandbox --project <project>. This sets up gVisor, the matching images, and the trusted worker, then enables the project service.
  2. Open the project in the dashboard and choose Sandboxes → Runs. Create a sandbox there, or from your server through the SDK or CLI. All three share the same run history.
  3. Open a sandbox to run code, inspect execution results, transfer files, and export artifacts.
  4. Export anything you need before terminating the sandbox or reaching its lifetime/idle deadline.

Choose a guide:

  • Dashboard and run history: visual status, executions, output, artifacts, and usage.
  • Sandbox client: TypeScript SDK, browser automation, files, and error handling.
  • Continue below for host requirements, isolation, REST endpoints, and recovery behavior.

Isolation and deployment

MVP uses Docker's gVisor runsc runtime, with Node.js 24 and Playwright 1.63.0 Chromium headless shell. Guest image installs Chromium only (--only-shell), without Firefox/WebKit or full GUI browser layers, reducing storage and download cost on the Pi. Ordinary containers alone are not a sufficient hostile-code boundary. The worker refuses to start without runsc, cgroup v2, and functioning resource controllers. There is no runc fallback. Kata and Firecracker provide VM boundaries but require more host/kernel/image machinery; gVisor is the smallest addition to this single-host Docker deployment. It still trusts gVisor, Docker, the worker, and the host kernel; it is not a guarantee against vulnerabilities in those components. Use a dedicated worker host when stronger separation from valuable host data is required.

Supported image architectures: Linux amd64 and arm64. Host must support a current gVisor release on that architecture. Chromium runs headless with its inner sandbox disabled inside gVisor; browser code shares the guest trust domain with submitted code. No guest is shared between projects. The guest has no host mounts, Docker socket, project credentials, model keys, application source, or network interface. Files and browser profiles live in bounded tmpfs mounts; root filesystem is read-only. CPU/memory/pids/no-new-privileges/capability limits apply to the complete guest.

Automatic host setup

Run this once on the Linux machine running the mybase Docker installation (for a Pi, SSH into it and run the command there). Add --project to enable the service immediately:

mybase server sandbox --project myapp

For a non-default installation location, add --dir /path/to/mybase. Without a terminal, add --yes. The equivalent shortcuts are mybase server setup --sandboxes during installation and mybase sandbox enable myapp --setup-worker for one project.

The host command automatically:

  • checks Linux amd64/arm64, a local rootful Docker daemon, Linux 5.6+, and cgroup v2 CPU, memory, swap, and process controllers;
  • downloads the official checksum-verified gVisor bundle if runsc is not registered, keeps its required sidecar binaries together, registers the runtime, and reloads Docker's runtime configuration without restarting existing containers (administrator/sudo access may be needed);
  • preserves the existing tunnel/profile settings and worker key, generates a key if missing, derives denied control-plane/storage hostnames and the server's public IP, and adds the sandbox Compose profile;
  • selects sandbox and worker images matching the installed API version (or commit-tagged master image), pulls missing images, and retains local/custom images without pulling them;
  • launches a temporary, resource-limited Chromium under runsc to verify the actual isolation path before saving settings, then starts the worker and applies the API configuration;
  • waits for worker/API health and optionally enables the project service.

No manual Docker commands, key generation, or .env editing are needed. Rerunning is safe: existing settings and keys are retained, already registered gVisor is reused, and existing images are not pulled again. mybase server update also pulls matching guest/worker images for automatically managed installations; the guest is not a Compose service, so this extra step is necessary. After host setup, creating and terminating individual sandboxes is automatic.

If public-IP detection is unavailable, pass --deny-hosts <public-router/server-IP>. Additional comma-separated infrastructure hostnames can be included. Custom local images use --image mybase-sandbox:local --worker-image mybase-sandbox-worker:local; build those from the repository with docker build -f docker/sandbox.Dockerfile -t mybase-sandbox:local . and docker build -f docker/sandbox-worker.Dockerfile -t mybase-sandbox-worker:local . first.

The command deliberately refuses Docker Desktop, rootless Docker, and remote Docker TCP endpoints: run it on the actual Docker host instead. It does not install Docker or change kernel/cgroup boot settings; if its prerequisite check identifies one of those problems, fix that host setting and rerun the same command. Existing custom/remote worker URLs are not overwritten.

Isolation boundary

Do not disable resource controls in runsc's configuration. Only one worker may manage a Docker daemon: startup removes all containers labelled mybase.sandbox=true as orphans. Guest creation never pulls an app-selected image. Worker port 4010 and browser port 9222 are never published. The Compose worker alone gets the Docker socket; it is trusted host administration code, not a sandbox. Its key authorizes privileged worker operations. Keep its network private. A remote worker needs an authenticated TLS tunnel/VPN and firewall restriction to the API; plain HTTP over an untrusted network is unsupported.

Default stack still starts without a worker. Default per-guest controls are 1 CPU, 1024 MiB memory, 256 pids, 128 MiB workspace, 256 MiB temporary browser space, and 64 MiB shm per guest. tmpfs counts against memory; swap is disabled. There are no project/global sandbox-count quotas or daily budgets. On a 4 GB Pi, one browser plus the backend can be tight; size concurrency for host headroom. No Kubernetes, nested Docker, or privileged guest is involved.

Host-owned resource and transfer controls

Set MYBASE_SANDBOX_CONFIG in the installation's .env to override per-guest controls. Compose passes the same value to the API and worker; for custom deployments, set it on both processes. Settings are read at startup. Restart/recreate the API and worker after changing them; new guests use the updated resource settings. Apps cannot override these controls.

MYBASE_SANDBOX_CONFIG='{"resources":{"cpus":2,"memoryMb":2048,"workspaceMb":512},"limits":{"fileBytes":33554432,"outputBytes":262144,"networkStreams":64}}'
GroupSettingDefaultMeaning
resourcescpus1CPU ceiling; positive fractions allowed
resourcesmemoryMb1024Guest memory ceiling, MiB; swap disabled
resourcesworkspaceMb128/workspace tmpfs size, MiB
resourcestmpMb256/tmp browser/tmpfs size, MiB
resourcesshmMb64Shared memory size, MiB
resourcespids256Process ceiling
limitsfileBytes8388608Maximum uploaded/downloaded/exported file size
limitsoutputBytes65536Captured bytes per stdout/stderr stream
limitsnetworkStreams32Simultaneous outbound TCP streams per guest
limitsnetworkIdleMs15000Outbound socket idle timeout

All values must be positive; values other than cpus must be safe integers. Omitted settings keep defaults; unknown names and invalid values fail startup. Resource controllers, path checks, public-only egress, and bounded protocol frames/buffers remain enforced. HTTP upload limits also apply (MYBASE_MAX_UPLOAD, default 1 GiB, and any reverse-proxy limit).

Network boundary

Guests use --network=none. A local HTTP proxy transports streams over docker exec stdio to the trusted worker. Only public unicast destinations on TCP 80/443 are allowed. Each connection resolves DNS, rejects the entire answer set if any address is non-public/denied, and connects directly to the selected IP. DNS is never resolved again during connect. IPv4 private, loopback, link-local, CGNAT, metadata, reserved ranges and IPv6 transition/private ranges are refused. Worker interface addresses plus configured infrastructure names and their resolved IPs are denied. List every public control-plane, storage, worker and router address/alias in DENY_HOSTS, including NAT public IPs; update/restart worker when infrastructure addresses change. Shared CDN IP denial may also block unrelated sites on that CDN.

The boundary covers redirects, subresources, browser WebSockets, and arbitrary executed code: there is no alternate NIC, DNS socket, UDP/QUIC, host gateway, or private route. Malicious code can bypass its proxy settings but then has no outbound transport. HTTP CONNECT supports HTTPS and WSS. Non-HTTP clients must explicitly use the local proxy. Node's bundled version may not honor proxy environment variables automatically: Playwright is configured explicitly; use its browser response APIs for downloads. Browser-local loopback services are inside the same guest. Guest code may exfiltrate anything deliberately supplied to it to public internet endpoints.

Default controls: 32 simultaneous network streams per sandbox and a 15-second idle socket timeout, both host-configurable. There is no lifetime stream-count or traffic-byte quota. Protocol frames and buffering remain bounded; relay failure or exceeded concurrent-stream/buffer limits terminates the guest.

API and lifecycle

All routes below require authentication and project-scoped sandbox:read or sandbox:write. Writes include execution, export, browser credential creation, and CDP upgrade (even though GET). Normal API permission/project revocation applies at every request/upgrade. No browser token alone grants access. Running CDP connections have no fixed lifetime; an explicitly configured sandbox deadline, guest termination, or transport failure can still close them.

Base: https://mybase.example.com/sandbox/<project>.

Method/pathResult
POST base {key, profile:"browser", ttlSeconds?, idleSeconds?}202, durable sandbox; poll GET
GET base; GET /<id>; GET /usagelist, state, usage
DELETE /<id>202, request termination; poll until terminal
POST /<id>/executions {key, code OR command:[], timeoutMs?}202, durable execution
GET /<id>/executions[/<execution>]bounded stdout/stderr and structured status
DELETE /<id>/executions/<execution>cancel; destroys entire guest/process tree
PUT/GET /<id>/files?path=<relative>binary upload/download, host-configured file cap (default 8 MiB)
POST /<id>/artifacts {path,type?}export file to private Garage storage
GET /<id>/artifacts[/<artifact>]metadata list / authenticated binary download
POST /<id>/browsersingle-use credential, 30-second redemption deadline
GET /<id>/browser (WebSocket)standard CDP; requires API auth + X-Mybase-Browser-Token

States: queued → provisioning → ready → stopping → terminated; TTL/idle → expired; provisioning, OOM, guest/relay loss, restart → failed. Terminal states never reopen. Lifetime (ttlSeconds), idle expiry (idleSeconds), and execution timeout (timeoutMs) are opt-in: omit or pass null for no deadline. Explicit values are positive integers in their named units, without a fixed policy ceiling. Lifetime starts at admission. Sandbox expiresAt is null when no lifetime was requested. Inspections do not reset an explicitly set idle timeout; work, transfers and browser connections count as activity. Worker enforces requested deadlines independently of API availability. Execution cannot outlive a requested sandbox lifetime. Explicit cancel/timeout destroys the entire sandbox so forked descendants cannot survive. Workspace persistence ends there.

One execution/transfer/CDP connection at a time per sandbox; conflicts return 409 rather than silently racing. Sequential successful commands share filesystem and browser state. ESM code runs with Node in /workspace, with playwright installed. Commands are argv arrays, with no implicit shell. No packages are installed from caller input. Output defaults to 64 KiB per stream with truncated (host-configurable); there is no execution-count quota. Logs are execution stdout/stderr; platform diagnostic logs never contain code, page content, URLs, browser headers, or artifacts.

Creation/execution require idempotency keys. Same key + same normalized input returns same ID; different input returns 409. Keys/metadata remain seven days. A lost execution dispatch response is never blindly replayed: sandbox is stopped and uncertain work is failed. Export is append-only; retrying export of the same path and bytes returns the same artifact. A changed file creates a new version.

Artifacts, usage, recovery

Artifacts are stored under a private _mybase/sandboxes/<id>/… prefix in the existing project bucket. They include project/sandbox IDs, MIME type, byte size, SHA-256, and seven-day expiration. Downloads always require authorization, are attachments with sandboxed content headers, and stream binary bytes. No images are embedded in application JSON. As with all project storage, the project's S3 credentials can access its own bucket (including sandbox artifacts). File transfers/exports default to 8 MiB/file (host-configurable). There are no artifact-count, per-sandbox aggregate byte, or daily export quotas. Pending exports count toward usage; failed/crashed exports are deleted and their usage accounting reversed after five minutes. Metadata is committed before object upload, so cleanup always knows the object key. Retention cleanup runs each minute; expired downloads fail immediately. Disabling service/project deletion stops guests before removing metadata/artifacts. Failed cleanup must be retried; resources are not declared released while Docker is unreachable.

Admission/idempotency is transactional in Postgres. There are no daily runtime/allocation budgets or sandbox-count quotas. Usage includes elapsed runtime, configured CPU/memory controls, executions, and exported bytes, attributed to the sandbox's UTC admission day. allocatedSecondsToday sums only explicitly requested lifetimes; it is accounting, not a budget. Legacy usage limits fields are null to mean unlimited. Audit records creation, execution requests, credential issuance, exports, terminal transitions and cancellation.

As with other mybase in-process runners, run one API instance. Scheduler polls once per second; HTTP requests only admit work, never own its lifetime. API restart reattaches worker's live sandboxes; missing runtimes fail. Worker restart kills labelled orphans before listening; all interrupted sandboxes fail on API reconciliation. Guest state is never checkpointed/replayed. Graceful API/worker shutdown requests guest destruction. After hard host failure, restart worker to remove orphans; Docker restart policy for guests is no, tmpfs is lost. No jobs auto-resume. Worker HTTP health is authenticated and reports active guests and configured resource controls after startup's runtime/controller validation; runtime exit/OOM and cleanup are checked each second.

Examples

mybase sandbox enable myapp
mybase token create myapp-sandbox --project myapp --perm sandbox:write
export MYBASE_API_KEY=mb_...
bun examples/sandbox/run-code.ts myapp script.mjs        # run a script, print stdout/stderr/exit code
bun examples/sandbox/html-to-pdf.ts myapp page.html out.pdf
bun examples/sandbox/page-capture.ts myapp               # built-in fixture page
bun examples/sandbox/page-capture.ts myapp https://example.com   # a live page (no reliability promise)

page-capture opens a page, expands content, records JSON and image responses and rendered text, takes a screenshot, and exports everything plus a manifest before terminating. Its selectors and heuristics are example code, not service features. Its diagnostics distinguish page_blocked_or_challenged, navigation_failure, content_loaded_information_absent, gallery_discovery_failure, and image_download_failure. Chromium does not guarantee access to sites that block automated browsers; challenges are observable outcomes, and no CAPTCHA bypass is supplied. agent-tools.ts maps execute and exportArtifact to tool definitions for any model provider.

Limitations and follow-up

No human takeover/live video, browser frames larger than 1 MiB, arbitrary runtimes, package installation API, private network allowlists, GPU, VM checkpoint, or multi-worker orchestration. CDP is Chromium-only and lower fidelity than Playwright's native protocol. A timed-out/cancelled execution loses the guest; export important files first. Artifact upload and DB commit use a pending/ready protocol; crashed pending exports are cleaned up and must be retried afterward. Worker retains completed execution results for ten minutes after termination; if API is unavailable longer, uncollected results can be lost and are marked failed on reconciliation. Operational follow-up: durable per-execution partial output, external worker fencing, and optional Kata VM profile.

Verification

bun run typecheck
bun test
bun scripts/test-sandboxes.ts
docker build -f docker/sandbox.Dockerfile -t mybase-sandbox:local .
bun scripts/test-sandbox-runtime.ts

API integration tests create disposable Postgres, Garage and Valkey containers and a deterministic worker double; existing databases are never migrated. Hardened-runtime checks require a configured gVisor host and cover real Chromium/CDP, network denial, timeout/cancel process-tree destruction, resource exhaustion, TTL and worker orphan recovery. .github/workflows/sandbox.yml runs these checks on native amd64 and arm64 hosts. On hosts without runsc, --fixture-only runs the trusted Chromium fixture and file helpers under runc; it does not verify hostile-code isolation. Live-site checks are opt-in by providing a URL to page-capture.ts, never part of deterministic CI.

Security

  • Only enable sandboxes where gVisor is installed, set timeouts unless you need open-ended runs, and treat artifacts and browser output as untrusted.

More: Security best practices.

Agent notes

  • Use @mybase-dev/sdk/sandbox and sandbox:write limited to your project; never put admin/model keys in code.
  • Poll sandbox/execution status; check terminal reason, exit status and truncation explicitly.
  • Fetch browser credentials just before connecting; pass headers to chromium.connectOverCDP.
  • Keep task-specific logic (what to run, which pages to visit, how to read results) in your application. Treat missing/blocked page content as a result, not a crash.
  • Export useful files before terminate/TTL; use artifact IDs for authorized downloads afterward.

On this page