# `Puck.Sandbox.Runtime`
[🔗](https://github.com/bradleygolden/puck/blob/v0.2.25/lib/puck/sandbox/runtime.ex#L1)

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:

- `Puck.Sandbox.Runtime.Adapters.Test` - In-memory testing adapter (shipped)
- Docker adapter - Planned
- Fly.io Machines adapter - Planned

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

# `backend`

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

# `file_block`

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

# `prompt_content`

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

# `text_block`

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

# `await_ready`

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`

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`

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`

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`

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`

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`

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`

Reads a file from the sandbox.

## Examples

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

# `start`

Starts a stopped sandbox.

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

## Examples

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

# `status`

Gets the current status of the sandbox.

## Examples

    :running = Runtime.status(sandbox)

# `stop`

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`

Terminates and cleans up the sandbox.

## Examples

    :ok = Runtime.terminate(sandbox)

# `update`

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`

Writes a file to the sandbox.

## Examples

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

# `write_files`

Writes multiple files to the sandbox.

## Examples

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
