Skip to content

xshellz/xshellz-go

Repository files navigation

xshellz-go

CI Go Reference License: MIT

The official Go SDK for xShellz sandboxes — spawn a real Linux box from your program, run commands in it, and throw it away (or keep it).

What is a sandbox? A sandbox is a small, private Linux machine that runs in the cloud, isolated from everyone else by a gVisor kernel — so whatever runs inside (including code an AI wrote) can't touch your computer or anyone else's. You get root, a real filesystem, and network access; when you're done you delete it.

Quickstart

1. Install the SDK

go get github.com/xshellz/xshellz-go@v0.2.0

2. Get an API key

Sign up at app.xshellz.com and create a personal access token with read and write scopes in your account settings (the API endpoint behind that page is POST /v1/auth/tokens). Then:

export XSHELLZ_API_KEY="<your token>"

3. Hello, sandbox

sbx, err := xshellz.Create(ctx, nil) // reads XSHELLZ_API_KEY
if err != nil { log.Fatal(err) }
defer sbx.Close(ctx) // destroys the box; call sbx.Detach() first to keep it

res, _ := sbx.Run(ctx, "echo hello from $(hostname)", nil)
fmt.Print(res.Stdout)

That's it — Create returns when the box is up, Run executes over SSH, and Close destroys the box.

Recipes

Run a command

res, err := sbx.Run(ctx, "python3 -c 'print(6*7)'", nil)
fmt.Print(res.Stdout)   // "42\n"
fmt.Print(res.ExitCode) // non-zero exit is DATA, not an error

Pass &xshellz.RunOptions{Cwd: "/srv", Env: map[string]string{"K": "v"}} to set a working directory or environment, and Stdout/Stderr writers to stream output live.

A permanent named box that survives restarts

GetOrCreate gives you the same box every time you run your program. The first call creates a box named "my-agent" and saves its SSH key to the local keystore (~/.xshellz/keys/my-agent.pem, chmod 0600); every later call finds the box by name, loads the key, and reattaches — starting the box first if it was idle-stopped.

sbx, err := xshellz.GetOrCreate(ctx, "my-agent", nil)
if err != nil { log.Fatal(err) }
defer sbx.Close(ctx) // a GetOrCreate box is already detached: Close just drops
                     // the SSH connection and leaves the box alive
// ...
// _ = sbx.Kill(ctx) // when you really want it gone: destroy it explicitly

A permanent box comes back already detached, so Close never destroys it — call sbx.Kill(ctx) to actually tear it down.

Security note: the keystore is a plaintext private key on disk (0600, in a 0700 dir). Anyone who can read the file can SSH into your box — delete the file to revoke local access. Set GetOrCreateOptions.KeystoreDir to relocate it, or DisableKeystore: true to manage keys yourself (then attaching to an existing box requires PrivateKeyPEM, and a missing key fails with ErrMissingKey).

Background jobs (spawn)

Spawn starts a command detached — it keeps running after Spawn returns and after your program exits, for as long as the box is up:

job, err := sbx.Spawn(ctx, "python3 -m http.server 8000", &xshellz.SpawnOptions{Name: "web"})

running, _ := job.IsRunning(ctx) // kill -0 probe
out, _ := job.Logs(ctx, 100)     // last 100 log lines (stdout+stderr)
_ = job.Stop(ctx)                // SIGTERM, then SIGKILL after ~5s grace

jobs, _ := sbx.Jobs(ctx) // list all spawned jobs + liveness

Logs live on the box under ~/.xshellz/jobs/<job-id>.log. Jobs do not survive a box Restart or stop.

Run AI-generated code safely (run_code)

Hand untrusted code to the box, not to your machine. RunCode writes the snippet to a temp file, runs the right interpreter, always deletes the file, and returns the same result type as Run:

res, err := sbx.RunCode(ctx, "python", "print(sum(range(10)))", nil)
fmt.Print(res.Stdout) // "45\n"; a traceback shows up as res.ExitCode != 0

Languages: python (python3), node, bash, ruby, php. Anything else returns ErrUnsupportedLanguage listing the supported ones.

Files up and down

_ = sbx.WriteFile(ctx, "/srv/config.json", []byte(`{"debug": true}`)) // bytes → box
data, _ := sbx.ReadFile(ctx, "/srv/report.txt")                       // box → bytes
_ = sbx.Upload(ctx, "./model.bin", "/srv/model.bin")                  // local file → box
_ = sbx.Download(ctx, "/srv/out.csv", "./out.csv")                    // box → local file

Binary-safe both ways; whole files are buffered in memory (sized for configs and artifacts, not multi-GB blobs).

Check resource usage (stats)

stats, _ := sbx.Stats(ctx)
fmt.Printf("mem %d/%d MB, cpu %.1f%%, disk %d/%d MB\n",
    stats.MemUsedMB, stats.MemAllowedMB, stats.CPUPercent,
    stats.DiskUsedMB, stats.DiskAllowedMB)

procs, _ := sbx.Procs(ctx) // top processes, SSH session count, detected agents

Open a web terminal in the browser

url, _ := sbx.TerminalURL(ctx)
fmt.Println(url) // open this in a browser for a live shell on the box

The URL embeds a signed token valid for about 1 hour — mint a fresh one each time instead of storing it.

Provisioning template (boxfile)

The account-level xshellz.box manifest is seeded into ~/xshellz.box on every newly created box — use it to preinstall dependencies so fresh boxes come up ready:

_, _ = xshellz.SetBoxfile(ctx, "apt: ffmpeg jq\npip: requests", nil)
manifest, _ := xshellz.GetBoxfile(ctx, nil)
_, _ = xshellz.SetBoxfile(ctx, "", nil) // empty clears it

Lifecycle

_ = sbx.Start(ctx)   // resume an idle-stopped box (/home preserved)
_ = sbx.Restart(ctx) // reboot a running box (/home preserved, processes are not)
sbx.Detach()         // make Close keep the box
_ = sbx.Close(ctx)   // drop SSH + destroy the box (unless detached)
_ = sbx.Kill(ctx)    // drop SSH + destroy the box unconditionally (even if detached)

Full API reference

Every public function, type, parameter, and error is documented in docs/API.md (and on pkg.go.dev).

Configuration

Env var 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 option (APIKey/APIURL on the options struct) > environment variable > default.

Errors

All sentinels work with errors.Is; API failures also carry an *xshellz.APIError (via errors.As) with the HTTP status, machine code, and message:

Sentinel When
ErrNoAPIKey no token in options or environment
ErrAuth 401/403 — bad token, missing scope, plan not entitled, verification required (APIError.Code == "verification_required")
ErrQuota the plan's concurrent box limit is reached — Connect/GetOrCreate the existing box instead
ErrNotFound 404 — unknown UUID, or Start on a box that isn't stopped
ErrNotRunning Run/Spawn/file ops on a stopped box — call sbx.Start(ctx)
ErrMissingKey GetOrCreate found the named box but no private key (keystore file missing or keystore disabled)
ErrUnsupportedLanguage RunCode with a language it doesn't know

v0 limits and design notes

  • Free tier: 1 concurrent box, which idle-stops after ~30 minutes. Creation is throttled to 10/min. Create returns ErrQuota while a box exists — Connect/GetOrCreate it (or Close it). A stopped box resumes with sbx.Start(ctx); /home is preserved.
  • Data plane is SSH (root@ssh_host:ssh_port, fake-root inside gVisor). Host-key checking uses ssh.InsecureIgnoreHostKey: boxes are ephemeral and mint a fresh host key each spawn, so there is nothing stable to pin yet. Treat the box as untrusted compute, not a secret store.
  • File transfer rides cat over SSH exec (binary-safe both ways) instead of SFTP, so the SDK works against any sshd and keeps golang.org/x/crypto as the only dependency.
  • Command execution: Run passes your command verbatim to the remote shell; with Cwd/Env set it wraps as cd CWD && env K=V ... sh -c 'CMD'.

Local development (Docker)

No local Go toolchain needed — run gofmt + go vet + the full test suite (with the ≥80% coverage gate) in a container (golang:1.22, module and build caches persisted in named volumes so re-runs are fast):

docker compose run --rm test

The repo is mounted at /work; GOMODCACHE and GOCACHE point at named volumes, so nothing is written into the repo itself except cover.out (gitignored).

License

MIT — see LICENSE.

About

Official Go SDK for xShellz sandboxes — spawn a throwaway, kernel-isolated Linux box and run commands in it from your code. go get github.com/xshellz/xshellz-go

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages