xshellz

Official Ruby SDK for xShellz sandboxes: throwaway, gVisor-isolated Linux boxes you can spawn and run commands in from your own program - in three lines.

gem install xshellz
require "xshellz"

Xshellz::Sandbox.create do |sbx|
  result = sbx.run("ruby -e 'puts 6*7'")
  puts result.stdout # 42
end
# the box is destroyed when the block exits

Each sandbox is a real Linux box (root shell, package manager, network) running under gVisor kernel isolation. Spawning is synchronous - Sandbox.create returns once the box is running, typically in a few seconds.

Authentication

The SDK authenticates with an xShellz personal access token (PAT) carrying the read and write scopes:

  1. Create a token from your xShellz dashboard (Settings -> API tokens), or via the API: POST /v1/auth/tokens.
  2. Export it:
export XSHELLZ_API_KEY="your-token"

or pass it explicitly: Xshellz::Sandbox.create(api_key: "your-token").

Config precedence: explicit argument > XSHELLZ_API_KEY / XSHELLZ_API_URL environment variables > default (https://api.xshellz.com/v1).

To target a staging or self-hosted control plane:

export XSHELLZ_API_URL="https://api.staging.example.com/v1"

Usage

Run commands

Xshellz::Sandbox.create(name: "build-box") 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:
  r = sbx.run("false")
  r.exit_code # => 1
  r.ok?       # => false

  # cwd and env:
  sbx.run("make test", cwd: "/srv/app", env: { "CI" => "1" })

  # Stream long-running output as it arrives:
  sbx.run("npm run build") do |stream, chunk|
    warn(chunk) if stream == :stderr
    print(chunk) if stream == :stdout
  end
end

Files (SFTP)

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

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

Lifecycle

sbx.uuid         # sandbox id
sbx.ssh_host     # e.g. "shellus1.xshellz.com"
sbx.ssh_port     # e.g. 42001
sbx.ssh_command  # ready-to-copy "ssh -p 42001 root@..."
sbx.status       # "running", "stopped", ...

sbx.detach       # keep the box alive after the create block
sbx.kill         # destroy the box explicitly
sbx.start        # resume an idle-stopped box

# Non-block form: you own the lifecycle.
sbx = Xshellz::Sandbox.create
sbx.run("uname -a")
sbx.kill

# Re-attach later (persist sbx.private_key_openssh + sbx.uuid for this):
sbx = Xshellz::Sandbox.connect(uuid, private_key: saved_private_key)

# Enumerate your sandboxes:
Xshellz::Sandbox.list.each do |info|
  puts "#{info.uuid} #{info.status}"
end

Typed errors

begin
  sbx = Xshellz::Sandbox.create
rescue Xshellz::QuotaError
  # plan limit reached - attach to the existing box instead
  existing = Xshellz::Sandbox.list.first
  sbx = Xshellz::Sandbox.connect(existing.uuid, private_key: saved_key)
rescue Xshellz::AuthError => e
  puts e.message # missing/invalid token, scope, or account verification
end
  • 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::SandboxNotRunningError - operation needs a running box
  • Xshellz::CommandTimeoutError - run(timeout: ...) exceeded
  • Xshellz::ApiError - any other 4xx/5xx (carries .status and .body)

How it works

  • Control plane: HTTPS to api.xshellz.com/v1 (create / list / start / destroy), authenticated by your PAT.
  • 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.

v0 limits

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

Local development (Docker)

No local Ruby toolchain is required - the whole test suite runs inside a ruby:3.3 container (gems cached in a named volume):

docker compose run --rm test

License

MIT