pocketenv-elixir

Elixir SDK for the Pocketenv sandbox platform.

Pocketenv lets you spin up isolated cloud sandbox environments on demand. This library wraps the Pocketenv XRPC API so you can manage sandboxes directly from your Elixir applications.


Installation

Add pocketenv_ex to your list of dependencies in mix.exs:

def deps do
  [
    {:pocketenv_ex, "~> 0.1"}
  ]
end
mix deps.get

Configuration

config/config.exs

import Config

config :pocketenv,
  token: "your-pocketenv-token",
  api_url: "https://api.pocketenv.io"   # optional — this is the default

Environment variables

export POCKETENV_TOKEN="your-pocketenv-token"
export POCKETENV_API_URL="https://api.pocketenv.io"   # optional

Application config takes precedence over environment variables.


Quick start

Pocketenv is the entry point. It returns %Sandbox{} structs that you pipe operations on:

{:ok, sandbox} =
  Pocketenv.create_sandbox("my-sandbox")
  |> Sandbox.start()
  |> Sandbox.wait_until_running()

{:ok, result} = sandbox |> Sandbox.exec("echo", ["hello"])
IO.puts(result.stdout)      # => "hello"

{:ok, url} = sandbox |> Sandbox.expose(3000)
IO.puts(url)                # => "https://3000-my-sandbox.sbx.pocketenv.io"

{:ok, vscode_url} = sandbox |> Sandbox.vscode()

sandbox
|> Sandbox.stop()
|> Sandbox.delete()

Every Sandbox function accepts either a bare %Sandbox{}or an {:ok, %Sandbox{}} tuple as its first argument, so you can pipe from any previous step without manually unwrapping.


API reference

All functions return {:ok, result} on success and {:error, reason} on failure. Every function accepts an optional :token keyword argument to override the globally configured token for that single call.


Pocketenv — entry point

Sandboxes

Function Returns Description
Pocketenv.create_sandbox(name, opts){:ok, %Sandbox{}} Create a new sandbox
Pocketenv.get_sandbox(id, opts){:ok, %Sandbox{} | nil} Fetch a sandbox by id or name
Pocketenv.list_sandboxes(opts){:ok, {[%Sandbox{}], total}} List the public sandbox catalog
Pocketenv.list_sandboxes_by_actor(did, opts){:ok, {[%Sandbox{}], total}} List all sandboxes for a user
create_sandbox/2 options
Option Type Default Description
:basestring official openclaw image AT-URI of the base sandbox image
:providerstring"cloudflare""cloudflare", "daytona", "deno", "vercel", or "sprites"
:repostringnil GitHub repo URL to clone on start
:keep_alivebooleannil Keep the sandbox alive after the session ends
:tokenstring global config Bearer token override
list_sandboxes/1 and list_sandboxes_by_actor/2 options
Option Type Default Description
:limitinteger30 Max results
:offsetinteger0 Pagination offset
:tokenstring global config Bearer token override

Actor / profile

Function Returns Description
Pocketenv.me(opts){:ok, %Profile{}} Fetch the authenticated user’s profile
Pocketenv.get_profile(did, opts){:ok, %Profile{}} Fetch any user’s profile by DID or handle
{:ok, me} = Pocketenv.me()
IO.puts("Logged in as @#{me.handle}")

{:ok, profile} = Pocketenv.get_profile("alice.bsky.social")

Sandbox — operations on a sandbox

All functions take a %Sandbox{} or {:ok, %Sandbox{}} as their first argument.

Lifecycle

Function Returns Description
Sandbox.start(sandbox, opts){:ok, %Sandbox{}} Start the sandbox, re-fetches state
Sandbox.stop(sandbox, opts){:ok, %Sandbox{}} Stop the sandbox, re-fetches state
Sandbox.delete(sandbox, opts){:ok, %Sandbox{}} Delete the sandbox permanently
Sandbox.wait_until_running(sandbox, opts){:ok, %Sandbox{}} Poll until status is :running

start/2 and stop/2 re-fetch the sandbox after the API call so the returned struct always has the latest status. delete/2 returns the last known state.

wait_until_running/2 options
Option Type Default Description
:timeout_msinteger60_000 Total wait time in ms
:interval_msinteger2_000 Polling interval in ms
:tokenstring global config Bearer token override

Commands

{:ok, result} = sandbox |> Sandbox.exec("mix", ["test", "--trace"])

IO.puts(result.stdout)
IO.puts(result.stderr)
IO.inspect(result.exit_code)
Function Returns Description
Sandbox.exec(sandbox, cmd, args \\ [], opts){:ok, %ExecResult{}} Run a shell command inside the sandbox

Ports

{:ok, url}   = sandbox |> Sandbox.expose(4000, description: "Phoenix")
{:ok, ports} = sandbox |> Sandbox.list_ports()
{:ok, _}     = sandbox |> Sandbox.unexpose(4000)
Function Returns Description
Sandbox.expose(sandbox, port, opts){:ok, url | nil} Expose a port publicly
Sandbox.unexpose(sandbox, port, opts){:ok, %Sandbox{}} Remove an exposed port
Sandbox.list_ports(sandbox, opts){:ok, [%Port{}]} List all exposed ports

VS Code

{:ok, url} = sandbox |> Sandbox.vscode()
IO.puts("Open VS Code at: #{url}")
Function Returns Description
Sandbox.vscode(sandbox, opts){:ok, url | nil} Expose VS Code Server and return its URL

If VS Code is already exposed the existing URL is returned immediately.


Types

%Sandbox{}

The central type of the SDK. Returned by Pocketenv.create_sandbox/2, Pocketenv.get_sandbox/2, and all Sandbox.* lifecycle functions.

%Sandbox{
  id:           String.t() | nil,
  name:         String.t() | nil,
  provider:     String.t() | nil,
  base_sandbox: String.t() | nil,
  display_name: String.t() | nil,
  uri:          String.t() | nil,
  description:  String.t() | nil,
  topics:       [String.t()] | nil,
  logo:         String.t() | nil,
  readme:       String.t() | nil,
  repo:         String.t() | nil,
  vcpus:        integer() | nil,
  memory:       integer() | nil,
  disk:         integer() | nil,
  installs:     integer(),
  status:       :running | :stopped | :unknown,
  started_at:   String.t() | nil,
  created_at:   String.t() | nil,
  owner:        %Sandbox.Types.Profile{} | nil
}

%Sandbox.Types.ExecResult{}

Returned by Sandbox.exec/4.

%Sandbox.Types.ExecResult{
  stdout:    String.t(),
  stderr:    String.t(),
  exit_code: integer()
}

%Sandbox.Types.Port{}

Returned in the list by Sandbox.list_ports/2.

%Sandbox.Types.Port{
  port:        integer(),
  description: String.t() | nil,
  preview_url: String.t() | nil
}

%Sandbox.Types.Profile{}

Returned by Pocketenv.me/1 and Pocketenv.get_profile/2.

%Sandbox.Types.Profile{
  id:           String.t() | nil,
  did:          String.t(),
  handle:       String.t(),
  display_name: String.t() | nil,
  avatar:       String.t() | nil,
  created_at:   String.t() | nil,
  updated_at:   String.t() | nil
}

Low-level client

If you need to call an endpoint not yet covered by the high-level API, use Pocketenv.Client directly:

{:ok, body} = Pocketenv.Client.get(
  "/xrpc/io.pocketenv.sandbox.getSandbox",
  params: %{"id" => "my-sandbox"},
  token: "override-token"
)

{:ok, body} = Pocketenv.Client.post(
  "/xrpc/io.pocketenv.sandbox.startSandbox",
  %{"keepAlive" => true},
  params: %{"id" => "my-sandbox"}
)

Running tests

mix test

The test suite does not make real HTTP calls. Integration tests that exercise the live API require a valid POCKETENV_TOKEN and are not included by default.


License

MIT