The official PHP SDK for xShellz sandboxes - spawn a real Linux box from your code, run commands in it, and throw it away (or keep it forever).
What is a sandbox? A sandbox is a small, disposable Linux computer in the cloud that your program fully controls: it has a root shell, a package manager, network access, and its own files, isolated from everyone else by gVisor. It is the safe place to run things you would not run on your own machine - installs, builds, untrusted or AI-generated code.
1. Install the SDK (PHP >= 8.2; pure PHP, no ext-ssh2 needed):
composer require xshellz/xshellz:^0.22. Get an API key. Sign up at app.xshellz.com
and create a personal access token with read and write scopes under
Settings -> API tokens (or via the API: POST /v1/auth/tokens). Then:
export XSHELLZ_API_KEY="your-token"3. Hello world:
use Xshellz\Sandbox;
$sbx = Sandbox::create();
try {
echo $sbx->run('echo hello from $(hostname)')->stdout;
} finally {
$sbx->kill(); // the box is destroyed
}Sandbox::create() returns once the box is running - typically a few
seconds. PHP has no context manager, so the idiomatic pattern is try/finally
with kill() as teardown; call detach() inside the block if you want to
keep the box.
$r = $sbx->run('apt-get update && apt-get install -y jq', timeoutSeconds: 300);
echo $r->exitCode, $r->stdout, $r->stderr;
// A non-zero exit code does NOT throw - it's data:
$r = $sbx->run('false');
assert($r->ok() === false);
// cwd, env, and live output streaming:
$sbx->run('make test', cwd: '/srv/app', env: ['CI' => '1']);
$sbx->run('npm run build', onStdout: fn (string $chunk) => print($chunk));Sandbox::getOrCreate() gives you a box by name: it creates it the first
time and re-attaches to the same box (same files, same key) every time after
- even from a different process, days later. The generated private key is
saved to a local keystore (
~/.xshellz/keys/, one 0600 file per box name) so nothing needs to be hand-carried between runs.
$sbx = Sandbox::getOrCreate('my-forever-box');
$sbx->run('echo persisted >> /root/notes.txt');
$sbx->detach(); // keep it alive
// ...tomorrow, in another script:
$sbx = Sandbox::getOrCreate('my-forever-box'); // same box, same notes.txtA stopped box (free boxes idle-stop after ~30 minutes) is started
automatically. Security note: the keystore holds plaintext private keys on
disk (0600, your user only) - anyone with the file has root SSH to that box;
delete the file to revoke. Pass keystore: false to disable persistence, or
privateKey: to supply a key yourself.
spawn() starts a process that keeps running after your script moves on -
servers, long builds, watchers:
$job = $sbx->spawn('python3 -m http.server 8000', name: 'web');
$job->pid; // process id inside the box
$job->isRunning(); // true while alive
echo $job->logs(50); // last 50 lines of its output
$job->stop(); // SIGTERM, then SIGKILL after a grace period
foreach ($sbx->jobs() as $j) { // every job log on the box
echo $j->id, ' running=', var_export($j->running, true), PHP_EOL;
}Hand runCode() a string of code - no shell-quoting, no temp-file
bookkeeping. It writes the code into the box, runs the right interpreter
(python -> python3, node, bash, ruby, php), and cleans up:
$r = $sbx->runCode('python', <<<'PY'
import json, urllib.request
print(json.dumps({"n": 6 * 7}))
PY);
echo $r->stdout; // {"n": 42}Anything the code does stays inside the sandbox - that is the point.
$sbx->writeFile('/tmp/config.json', '{"debug": true}');
$data = $sbx->readFile('/tmp/config.json'); // string (binary-safe)
$sbx->upload('local.txt', '/tmp/remote.txt');
$sbx->download('/tmp/remote.txt', 'out.txt');$stats = $sbx->stats(); // mem_used_mb, cpu_percent, disk_used_mb, pids_current, ...
$procs = $sbx->procs(); // top processes + active SSH session count
$sbx->restart(); // reboot the box; /home is preservedecho $sbx->terminalUrl(); // open in any browser for a root terminalThe URL embeds a signed token that expires after about an hour - mint a fresh one each time instead of storing it.
The account-level boxfile is a template applied whenever a NEW box is created - use it to preinstall your dependencies so destroy+recreate always reproduces your environment:
Xshellz\Sandbox::setBoxfile("jq\nripgrep\npython3-pip");
echo Xshellz\Sandbox::getBoxfile();Every public method, its parameters, return shapes, and errors: docs/API.md.
| 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 |
Precedence: explicit argument > environment variable > default.
All SDK errors extend Xshellz\Exceptions\XshellzException, so one catch
gets everything.
| Exception | When |
|---|---|
AuthException |
401/403: bad or missing token, scopes, account gates |
QuotaException |
Plan sandbox limit reached / plan has no sandbox entitlement |
MissingKeyException |
getOrCreate() found the box but no private key for it |
SandboxNotRunningException |
The operation needs a running box |
CommandTimeoutException |
run(timeoutSeconds: ...) exceeded |
ApiException |
Any other 4xx/5xx (carries ->statusCode and ->body) |
- 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(via phpseclib).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'sauthorized_keys. - Host keys are auto-accepted. 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->sshCommand.
- Free tier: 1 concurrent sandbox. A second
Sandbox::create()throwsQuotaExceptionwhile one exists - usegetOrCreate()/connect()to attach to the existing box, orkill()it first. Paid plans raise the limit. - Free boxes idle-stop after ~30 minutes. The box (its files and your
key) is preserved;
start()- orgetOrCreate()- resumes it. - Sandbox creation is throttled to 10 requests/minute per account.
No local PHP needed - the test suite (with its >= 80% coverage gate) runs in a container:
docker compose run --rm testThis builds a PHP 8.3 CLI image with the pcov coverage driver, runs
composer install, and executes the Pest suite against the repo mounted at
/work. The composer cache is kept in a named volume between runs. The
container runs as uid/gid 1000 by default so it never leaves root-owned
files in your checkout; override with DOCKER_UID/DOCKER_GID if your user
differs.
Without Docker:
composer install
composer test # plain run
composer test:coverage # with the >= 80% coverage gate (needs pcov/xdebug)MIT