Skip to content

Commit e59eb56

Browse files
authored
Merge pull request #2 from pepicrft/add-reconnect-serialization
feat: Add sandbox serialization and reconnect support
2 parents 04623b7 + 39b25ca commit e59eb56

File tree

16 files changed

+1079
-33
lines changed

16 files changed

+1079
-33
lines changed

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@
33
## Workflow
44

55
- After every change, create a git commit and push it to the current branch.
6+
7+
## Testing
8+
9+
- Never modify global state in tests (e.g. `Application.put_env`, `Application.delete_env`). All tests must be safe to run with `async: true`.
10+
- For running bash commands from Elixir, use `MuonTrap` instead of `System`. Prefer `MuonTrap` because it propagates process shutdowns to child processes. Reference: https://hexdocs.pm/muontrap/readme.html

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

README.md

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ The AI agent ecosystem is producing many sandbox environment providers — Dayto
1515
- **Provider behaviour** — a single contract for creating, destroying, and querying sandbox environments
1616
- **Process execution** — run commands in sandboxes with structured results
1717
- **File operations** — read, write, and list files within sandboxes
18+
- **Named providers** — configure multiple providers with their credentials, pick a default
19+
- **Local provider** — built-in provider for dev/test that runs everything on the local machine
20+
- **Serialization** — persist and restore sandbox references across client restarts
1821
- **Provider-agnostic** — swap providers without changing application code
1922

2023
## Installation
@@ -29,6 +32,46 @@ def deps do
2932
end
3033
```
3134

35+
## Configuration
36+
37+
Configure multiple providers and set a default, similar to Finch pools:
38+
39+
```elixir
40+
# config/runtime.exs
41+
config :terrarium,
42+
default: :daytona,
43+
providers: [
44+
daytona: {Terrarium.Daytona, api_key: System.fetch_env!("DAYTONA_API_KEY"), region: "us"},
45+
e2b: {Terrarium.E2B, api_key: System.fetch_env!("E2B_API_KEY")},
46+
local: Terrarium.Providers.Local
47+
]
48+
```
49+
50+
Connect to an existing machine via SSH:
51+
52+
```elixir
53+
config :terrarium,
54+
default: :server,
55+
providers: [
56+
server: {Terrarium.Providers.SSH,
57+
host: "dev.example.com",
58+
user: "deploy",
59+
auth: {:key, System.fetch_env!("SSH_PRIVATE_KEY")}
60+
}
61+
]
62+
```
63+
64+
For development, use the built-in local provider:
65+
66+
```elixir
67+
# config/dev.exs
68+
config :terrarium,
69+
default: :local,
70+
providers: [
71+
local: Terrarium.Providers.Local
72+
]
73+
```
74+
3275
## Quick Start
3376

3477
### 1. Add a provider package
@@ -45,11 +88,14 @@ end
4588
### 2. Create and use a sandbox
4689

4790
```elixir
48-
# Create a sandbox
49-
{:ok, sandbox} = Terrarium.create(Terrarium.Daytona,
50-
image: "debian:12",
51-
resources: %{cpu: 2, memory: 4}
52-
)
91+
# Uses the configured default provider
92+
{:ok, sandbox} = Terrarium.create(image: "debian:12")
93+
94+
# Or use a specific named provider
95+
{:ok, sandbox} = Terrarium.create(:e2b, image: "debian:12")
96+
97+
# Or pass a provider module directly
98+
{:ok, sandbox} = Terrarium.create(Terrarium.Daytona, image: "debian:12", api_key: "...")
5399

54100
# Execute commands
55101
{:ok, result} = Terrarium.exec(sandbox, "echo hello")
@@ -63,6 +109,21 @@ IO.puts(result.stdout)
63109
:ok = Terrarium.destroy(sandbox)
64110
```
65111

112+
### 3. Surviving client restarts
113+
114+
Sandboxes can be serialized and restored if the client process restarts while the remote sandbox is still running:
115+
116+
```elixir
117+
# Persist before shutdown
118+
data = Terrarium.Sandbox.to_map(sandbox)
119+
MyStore.save("sandbox-123", data)
120+
121+
# Restore after restart
122+
data = MyStore.load("sandbox-123")
123+
sandbox = Terrarium.Sandbox.from_map(data)
124+
{:ok, sandbox} = Terrarium.reconnect(sandbox)
125+
```
126+
66127
## Implementing a Provider
67128

68129
Providers implement the `Terrarium.Provider` behaviour:
@@ -88,6 +149,12 @@ defmodule MyProvider do
88149
:running
89150
end
90151

152+
@impl true
153+
def reconnect(sandbox) do
154+
# Verify the sandbox is still alive, refresh tokens, etc.
155+
{:ok, sandbox}
156+
end
157+
91158
@impl true
92159
def exec(sandbox, command, opts) do
93160
# Execute the command
@@ -111,12 +178,44 @@ end
111178

112179
| Provider | Package | Status |
113180
|---|---|---|
181+
| Local | `terrarium` (built-in) | Available |
182+
| SSH | `terrarium` (built-in) | Available |
114183
| [Daytona](https://daytona.io) | `terrarium_daytona` | Planned |
115184
| [E2B](https://e2b.dev) | `terrarium_e2b` | Planned |
116185
| [Modal](https://modal.com) | `terrarium_modal` | Planned |
117186
| [Fly Sprites](https://sprites.dev) | `terrarium_sprites` | Planned |
118187
| [Namespace](https://namespace.so) | `terrarium_namespace` | Planned |
119188

189+
## Telemetry
190+
191+
Terrarium emits telemetry events for all operations via `:telemetry.span/3`. Each operation emits `:start`, `:stop`, and `:exception` events automatically.
192+
193+
| Event | Metadata |
194+
|---|---|
195+
| `[:terrarium, :create, *]` | `%{provider: module}` |
196+
| `[:terrarium, :destroy, *]` | `%{sandbox: sandbox}` |
197+
| `[:terrarium, :exec, *]` | `%{sandbox: sandbox, command: string}` |
198+
| `[:terrarium, :read_file, *]` | `%{sandbox: sandbox, path: string}` |
199+
| `[:terrarium, :write_file, *]` | `%{sandbox: sandbox, path: string}` |
200+
| `[:terrarium, :ls, *]` | `%{sandbox: sandbox, path: string}` |
201+
| `[:terrarium, :reconnect, *]` | `%{sandbox: sandbox}` |
202+
| `[:terrarium, :status, *]` | `%{sandbox: sandbox}` |
203+
204+
```elixir
205+
:telemetry.attach_many(
206+
"terrarium-logger",
207+
[
208+
[:terrarium, :create, :stop],
209+
[:terrarium, :exec, :stop],
210+
[:terrarium, :destroy, :stop]
211+
],
212+
fn event, measurements, metadata, _config ->
213+
Logger.info("#{inspect(event)} took #{measurements.duration} native time units")
214+
end,
215+
nil
216+
)
217+
```
218+
120219
## License
121220

122221
This project is licensed under the [MIT License](LICENSE).

0 commit comments

Comments
 (0)