Puck.Sandbox.Runtime (Puck v0.2.11)

Copy Markdown View Source

Container-based sandbox management for isolated code execution.

Puck.Sandbox.Runtime provides a struct-based API for creating and managing isolated container environments. Inspired by E2B's simplicity and Modal's from_id pattern.

For in-process code evaluation (Lua, JavaScript), see Puck.Sandbox.Eval.

Usage

alias Puck.Sandbox.Runtime
alias Puck.Sandbox.Runtime.Adapters.Test

# Create a sandbox
{:ok, sandbox} = Runtime.create({Test, image: "node:22-slim"})

# Execute commands
{:ok, result} = Runtime.exec(sandbox, "node --version")
IO.puts(result.stdout)

# Cleanup
:ok = Runtime.terminate(sandbox)

Adapters

Runtime uses an adapter pattern for different backends:

Custom adapters can be created by implementing the Puck.Sandbox.Runtime.Adapter behaviour.

Summary

Functions

Waits for sandbox to become ready.

Creates a new sandbox using the specified adapter and configuration.

Creates a new sandbox from a template with optional config overrides.

Executes a command in the sandbox.

Reconstructs a sandbox struct from an existing sandbox ID.

Gets the URL for an exposed port on the sandbox.

Sends a prompt to the sandbox server and returns a stream of events.

Reads a file from the sandbox.

Starts a stopped sandbox.

Gets the current status of the sandbox.

Stops a sandbox without destroying it.

Terminates and cleans up the sandbox.

Updates a sandbox's configuration without destroying it.

Writes a file to the sandbox.

Writes multiple files to the sandbox.

Types

backend()

@type backend() :: {module(), map() | keyword()}

file_block()

@type file_block() ::
  %{type: :file, media_type: String.t(), data: String.t()}
  | %{type: :file, media_type: String.t(), text: String.t()}

prompt_content()

@type prompt_content() :: String.t() | [text_block() | file_block()]

text_block()

@type text_block() :: %{type: :text, text: String.t()}

Functions

await_ready(sandbox, opts \\ [])

Waits for sandbox to become ready.

Delegates to the adapter's await_ready/3 if implemented. Falls back to polling the health endpoint if not.

Options

  • :port - health check port (default: 4001)
  • :timeout - max wait time in ms (default: 60_000)
  • :interval - poll interval in ms (default: 2_000)

Examples

{:ok, metadata} = Runtime.await_ready(sandbox)
{:ok, metadata} = Runtime.await_ready(sandbox, timeout: 30_000)

create(arg)

Creates a new sandbox using the specified adapter and configuration.

Examples

{:ok, sandbox} = Runtime.create({Docker, image: "node:22-slim"})

{:ok, sandbox} = Runtime.create({Docker, %{
  image: "node:22-slim",
  workdir: "/workspace",
  memory_mb: 2048
}})

create(template, overrides \\ %{})

Creates a new sandbox from a template with optional config overrides.

Examples

template = Template.new({Docker, %{image: "python:3.12", memory_mb: 512}})

# Create from template
{:ok, sandbox} = Runtime.create(template)

# With overrides
{:ok, sandbox} = Runtime.create(template, memory_mb: 1024)

exec(sandbox, command, opts \\ [])

Executes a command in the sandbox.

Options

  • :timeout - Command timeout in milliseconds (default: 30_000)
  • :workdir - Working directory for the command

Examples

{:ok, result} = Runtime.exec(sandbox, "node --version")
IO.puts(result.stdout)

# With timeout
{:ok, result} = Runtime.exec(sandbox, "npm install", timeout: 120_000)

# File operations via exec
{:ok, _} = Runtime.exec(sandbox, "echo 'hello' > file.txt")
{:ok, result} = Runtime.exec(sandbox, "cat file.txt")

from_id(adapter_module, sandbox_id, config \\ %{}, metadata \\ %{})

Reconstructs a sandbox struct from an existing sandbox ID.

Useful for resuming work with a container that was created elsewhere or persisted across sessions. Inspired by Modal's Sandbox.from_id().

Examples

sandbox = Runtime.from_id(Docker, "puck-sandbox-abc123")
{:ok, result} = Runtime.exec(sandbox, "echo 'still here'")

get_url(instance, port)

Gets the URL for an exposed port on the sandbox.

Useful for sandboxes running servers. This is an optional adapter callback - returns {:error, :not_implemented} if the adapter doesn't support it.

Examples

{:ok, url} = Runtime.get_url(sandbox, 4000)
# => "http://puck-sandbox-abc123:4000"

prompt(sandbox, content, opts \\ [])

Sends a prompt to the sandbox server and returns a stream of events.

The sandbox must be running a puck-sandbox compatible server that accepts POST requests to /prompt and returns NDJSON events.

Options

  • :port - The port the sandbox server is listening on (default: 4001)
  • :timeout - Request timeout in milliseconds (default: 60_000)
  • :options - Custom options passed to the sandbox server (default: %{})

Event Format

The stream yields maps with a "type" key indicating the event type:

%{"type" => "text", "text" => "Hello!"}
%{"type" => "error", "message" => "..."}

Examples

{:ok, stream} = Runtime.prompt(sandbox, "Hello")
Enum.each(stream, fn event ->
  case event do
    %{"type" => "text", "text" => text} -> IO.write(text)
    %{"type" => "error", "message" => msg} -> IO.puts("Error: #{msg}")
    _ -> :ok
  end
end)

# With content blocks for images/files
{:ok, stream} = Runtime.prompt(sandbox, [
  %{type: "text", text: "What's in this image?"},
  %{type: "file", media_type: "image/png", data: Base.encode64(bytes)}
])

# Accumulate full response
{:ok, stream} = Runtime.prompt(sandbox, "Write a poem")
text = stream
  |> Enum.filter(&match?(%{"type" => "text"}, &1))
  |> Enum.map_join(&Map.get(&1, "text"))

read_file(instance, path, opts \\ [])

Reads a file from the sandbox.

Examples

{:ok, content} = Runtime.read_file(sandbox, "/app/code.py")

start(instance, opts \\ [])

Starts a stopped sandbox.

Returns {:error, :not_implemented} if the adapter doesn't support it.

Examples

{:ok, _} = Runtime.start(sandbox)

status(sandbox)

Gets the current status of the sandbox.

Examples

:running = Runtime.status(sandbox)

stop(instance, opts \\ [])

Stops a sandbox without destroying it.

Useful for suspend/resume patterns. The sandbox can be restarted with start/1. Returns {:error, :not_implemented} if the adapter doesn't support it.

Examples

{:ok, _} = Runtime.stop(sandbox)

terminate(sandbox)

Terminates and cleans up the sandbox.

Examples

:ok = Runtime.terminate(sandbox)

update(instance, config, opts \\ [])

Updates a sandbox's configuration without destroying it.

This preserves state like volume attachments. Returns {:error, :not_implemented} if the adapter doesn't support it.

Examples

{:ok, _} = Runtime.update(sandbox, %{image: "node:23-slim"})

write_file(instance, path, content, opts \\ [])

Writes a file to the sandbox.

Examples

:ok = Runtime.write_file(sandbox, "/app/code.py", "print('hello')")

write_files(sandbox, files, opts \\ [])

Writes multiple files to the sandbox.

Examples

:ok = Runtime.write_files(sandbox, [
  {"/app/main.py", "import lib"},
  {"/app/lib.py", "def foo(): pass"}
])