ask-sandbox-providers

Gem Version

Sandbox providers for the ask-rb ecosystem. Isolated code execution via four backends:

Provider Isolation Speed Requirement
Local Process + rlimits (CPU, memory, processes, file size) Instant None (stdlib)
Docker Full container (read-only rootfs, no network, no capabilities) ~1s Docker daemon
Daytona Remote container via Daytona API ~2-5s daytona gem, API key
Cloudflare Cloudflare Workers sandbox via proxy Worker ~1-3s Proxy Worker URL

Installation

gem "ask-sandbox-providers"

Quick Start

require "ask-sandbox-providers"

# Default: Local (subprocess + rlimits on macOS/Linux)
result = Ask::Sandbox.provider.call(["ruby", "-e", "puts 1+1"])
puts result.stdout  # => "1\n"
puts result.exit_code  # => 0
puts result.timed_out  # => false

Configure a different provider

# Docker (requires Docker daemon)
Ask::Sandbox.provider = Ask::Sandbox::Docker.new(
  image: "ruby:3.4-alpine",
  memory: "256m",
  network: false
)

# Daytona (requires `daytona` gem + API key)
Ask::Sandbox.provider = Ask::Sandbox::Daytona.new(
  api_key: Ask::Auth.lookup("DAYTONA_API_KEY"),
  server_url: "https://api.daytona.io"
)

# Cloudflare (requires a deployed proxy Worker)
Ask::Sandbox.provider = Ask::Sandbox::Cloudflare.new(
  worker_url: "https://sandbox-proxy.my-worker.workers.dev",
  auth_token: ENV["CLOUDFLARE_SANDBOX_TOKEN"]
)

String vs Array commands

# String → executed via shell (bash -c)
Ask::Sandbox.provider.call("ls -la | head -5")

# Array → executed directly, no shell (safer for untrusted input)
Ask::Sandbox.provider.call(["ruby", "-e", "puts ENV['HOME']"])

Return value

All providers return an Ask::Sandbox::Result:

Result = Data.define(:stdout, :stderr, :exit_code, :timed_out)

Provider Details

Local

The default provider. Runs commands in a temp directory with:

  • Process group isolation — all child processes are killed on timeout
  • Process.setrlimit — CPU (10s), address space (2GB), processes (50), file size (10MB), FDs (200)
  • Temp directory — execution sandbox is auto-cleaned
  • Environment sanitization — Bundler/Ruby env vars stripped

Available on every platform where Ruby runs (macOS, Linux). Zero external dependencies.

Docker

Runs commands in a Docker container with security hardening:

  • --read-only — read-only root filesystem
  • --cap-drop ALL — no Linux capabilities
  • --security-opt no-new-privileges — no privilege escalation
  • --network none — no network egress (configurable)
  • --memory, --cpus, --pids-limit — resource limits
  • --rm — auto-cleanup on exit

Daytona

Runs commands in a Daytona sandbox via the official daytona gem. Daytona provides secure, elastic sandboxes with full isolation, dedicated kernel, filesystem, and network stack. See daytona.io/docs.

Cloudflare

Runs commands in a Cloudflare Workers sandbox. Requires deploying a proxy Worker that wraps @cloudflare/sandbox. See the Cloudflare Sandbox SDK docs for setup.

Migration from Direct Open3 Usage

If you're upgrading from ask-tools-shell v0.1.0 where Code and Bash tools called Open3.popen3 directly — no action needed. The default Ask::Sandbox::Local provider behaves identically. To enable stronger isolation, switch the provider:

# Before: direct Open3 (implicit Local)
Ask::Tools::Code.new.call(code: "puts 1")

# After: configure Docker for stronger isolation
Ask::Sandbox.provider = Ask::Sandbox::Docker.new
# The Code tool now runs code inside a Docker container automatically

Development

git clone https://github.com/ask-rb/ask-sandbox-providers.git
cd ask-sandbox-providers
bundle install
bundle exec rake test

License

MIT