xshellz

CI Gem Version License: MIT

The official Ruby SDK for xShellz sandboxes: spawn a real Linux box from your program, run anything in it, throw it away.

What is a sandbox? A sandbox is a small, disposable Linux computer that runs in the cloud, isolated from everything else (including your machine), so whatever runs inside it cannot touch your files or your network. That makes it the safe place to run untrusted things: AI-generated code, build scripts, scrapers, other people's projects.

Each xShellz sandbox is a full Linux environment - root shell, package manager, network access - running under gVisor kernel isolation. Spawning is synchronous: Sandbox.create returns once the box is reachable, typically in a few seconds.

Quickstart

  1. Install the gem:

    gem install xshellz
    
  2. Get an API key. Sign up at app.xshellz.com, then create a personal access token with read and write scopes under Settings -> API tokens (or via the API: POST /v1/auth/tokens). Export it:

    export XSHELLZ_API_KEY="your-token"
    
  3. Hello world:

    require "xshellz"
    
    Xshellz::Sandbox.create do |sbx|
      puts sbx.run("echo hello from $(hostname)").stdout
    end
    # the box is destroyed when the block exits
    

Recipes

Run a command

Xshellz::Sandbox.create do |sbx|
  r = sbx.run("apt-get update && apt-get install -y jq", timeout: 300)
  puts r.exit_code, r.stdout, r.stderr

  # A non-zero exit code does NOT raise - it's data, like a local subprocess:
  sbx.run("false").ok? # => false

  # cwd, env, and live output streaming:
  sbx.run("make test", cwd: "/srv/app", env: { "CI" => "1" }) do |stream, chunk|
    print(chunk) if stream == :stdout
  end
end

A permanent named box that survives restarts

get_or_create finds your box by name, or creates it, and remembers the SSH key for you in ~/.xshellz/keys/ - so the same line works on every run, from any process, forever:

sbx = Xshellz::Sandbox.get_or_create("my-dev-box")
sbx.run("echo persists across runs > ~/note.txt")
# ... later, in a different script or on a different day:
sbx = Xshellz::Sandbox.get_or_create("my-dev-box") # same box, same files

A stopped (idled) box is started automatically. Keys are plaintext files with 0600 permissions - delete the key file and kill the box to revoke, or pass keystore: false to keep keys purely in memory.

Run a background job

spawn starts a process that keeps running after your script disconnects:

job = sbx.spawn("python3 train.py", name: "train")
job.pid       # => 4242
job.running?  # => true
job.logs(tail_lines: 20) # last 20 lines of combined stdout+stderr
job.stop      # SIGTERM, then SIGKILL after a grace period

sbx.jobs.each { |j| puts "#{j.id} running=#{j.running?}" }

Run AI-generated code safely

run_code writes a snippet to a temp file in the sandbox, executes the right interpreter, and always cleans the file up. If the code is malicious, it can only harm a disposable box:

result = sbx.run_code("python", generated_code, timeout: 60)
puts result.stdout

Supported languages: python (python3), node, bash, ruby, php.

Files: up and down

sbx.write_file("/tmp/config.json", '{"debug": true}')
sbx.read_file("/tmp/config.json") # => String

sbx.upload("local.txt", "/tmp/remote.txt")
sbx.download("/tmp/remote.txt", "out.txt")

Check resource usage

stats = sbx.stats
puts "mem #{stats.mem_used_mb}/#{stats.mem_allowed_mb} MB, cpu #{stats.cpu_percent}%"

top = sbx.procs
top.procs.each { |p| puts "#{p.pid} #{p.comm} #{p.cpu}%" }

Open a web terminal

Get a browser-ready root shell for the box - no SSH client needed. The signed URL expires after about an hour; mint a fresh one each time:

puts sbx.terminal_url

Provision every new box the same way (boxfile)

The account-level boxfile is a package manifest applied whenever a NEW box is created - preinstall your dependencies once:

Xshellz::Sandbox.set_boxfile(<<~BOX)
  apt jq ripgrep
  pip requests
BOX
Xshellz::Sandbox.get_boxfile # => the saved manifest

API reference

Every public method, its parameters, return shapes, and errors are documented in docs/API.md.

Configuration

Environment variable Meaning Default
XSHELLZ_API_KEY Personal access token (read + write scopes) - (required)
XSHELLZ_API_URL Control-plane base URL https://api.xshellz.com/v1

Explicit arguments (api_key:, api_url:) always win over the environment.

Error types

Error Meaning
Xshellz::Error Base class for everything the SDK raises
Xshellz::AuthError 401/403: bad or missing token, scopes, account gates
Xshellz::QuotaError Plan sandbox limit reached / plan has no sandbox entitlement
Xshellz::MissingKeyError get_or_create found the box but no private key for it
Xshellz::SandboxNotRunningError Operation needs a running box
Xshellz::CommandTimeoutError run(timeout: ...) exceeded
Xshellz::UnsupportedLanguageError run_code got a language it doesn't know
Xshellz::ApiError Any other 4xx/5xx (carries .status and .body)

v0 limits

  • Free tier: 1 concurrent sandbox. A second Sandbox.create raises Xshellz::QuotaError while one exists - attach to the existing box instead (get_or_create / Sandbox.connect), or kill it first.
  • Free boxes idle-stop after ~30 minutes. The box (its /home and your key) is preserved; sbx.start - or get_or_create - resumes it.
  • Sandbox creation is throttled to 10 requests/minute per account.

How it works

  • Control plane: HTTPS to api.xshellz.com/v1 (create / list / start / destroy / stats), authenticated by your token.
  • Data plane: SSH directly to the box as root (net-ssh), files over SFTP (net-sftp). Sandbox.create generates an in-memory ed25519 keypair per sandbox; the private key never leaves your process and the server never sees it - only the public half is installed in the box's authorized_keys.
  • Host keys are auto-accepted (verify_host_key: :never). Sandbox host keys are generated at spawn time, so there is no out-of-band fingerprint to pin. If your threat model requires host-key verification, connect manually with your own SSH tooling using sbx.ssh_command.

Local development (Docker)

No local Ruby toolchain is required - the whole test suite (with the >= 80% coverage gate) runs inside a ruby:3.3 container, gems cached in a named volume:

docker compose run --rm test

License

MIT