e2b 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +181 -0
- data/lib/e2b/api/http_client.rb +164 -0
- data/lib/e2b/client.rb +201 -0
- data/lib/e2b/configuration.rb +119 -0
- data/lib/e2b/errors.rb +88 -0
- data/lib/e2b/models/entry_info.rb +243 -0
- data/lib/e2b/models/process_result.rb +127 -0
- data/lib/e2b/models/sandbox_info.rb +94 -0
- data/lib/e2b/sandbox.rb +407 -0
- data/lib/e2b/services/base_service.rb +485 -0
- data/lib/e2b/services/command_handle.rb +350 -0
- data/lib/e2b/services/commands.rb +229 -0
- data/lib/e2b/services/filesystem.rb +373 -0
- data/lib/e2b/services/git.rb +893 -0
- data/lib/e2b/services/pty.rb +297 -0
- data/lib/e2b/services/watch_handle.rb +110 -0
- data/lib/e2b/version.rb +5 -0
- data/lib/e2b.rb +87 -0
- metadata +142 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d76a27510ea8da9f113ba2efb9aecf7c06377d20a409153491f6a918ee506a72
|
|
4
|
+
data.tar.gz: c0b1dae2a55304bbddd9211a54dea1ba964be0dcf73557a479f936627ebd67c3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8777bb85409fab9f15c62c7d8d39856c7901616e90cc150914b78deaf4e71ba32540545dced53513cfee7641fa234815254ff529f5ebd41f533513696af436de
|
|
7
|
+
data.tar.gz: b1bd243b3741b8cba7860ffc36f0398ed5b049cb82482d65a1fea0f5da55071d3664602b3144ff7cbb06c4c90708e991ae565c9ba032ced51dac0f4e0bd6e2fb
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Voyager Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# E2B Ruby SDK
|
|
2
|
+
|
|
3
|
+
Ruby SDK for [E2B](https://e2b.dev) - secure cloud sandboxes for AI-generated code execution.
|
|
4
|
+
|
|
5
|
+
Aligned with the [official E2B SDKs](https://github.com/e2b-dev/E2B) (Python/JS).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Sandbox lifecycle management (create, connect, pause, resume, kill)
|
|
10
|
+
- Command execution with streaming output and background processes
|
|
11
|
+
- Filesystem operations via proper envd RPC (list, read, write, stat, watch)
|
|
12
|
+
- PTY (pseudo-terminal) support for interactive sessions
|
|
13
|
+
- Git operations (clone, push, pull, branches, status, commit, etc.)
|
|
14
|
+
- Directory watching with event polling
|
|
15
|
+
- Snapshot support
|
|
16
|
+
- Backward-compatible Client class
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add to your Gemfile:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem 'e2b', git: 'https://github.com/ya-luotao/e2b-ruby.git'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bundle install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
require 'e2b'
|
|
36
|
+
|
|
37
|
+
# Create a sandbox (matches official SDK pattern)
|
|
38
|
+
sandbox = E2B::Sandbox.create(template: "base", api_key: ENV['E2B_API_KEY'])
|
|
39
|
+
|
|
40
|
+
# Run commands
|
|
41
|
+
result = sandbox.commands.run('echo "Hello from E2B!"')
|
|
42
|
+
puts result.stdout
|
|
43
|
+
|
|
44
|
+
# Work with files
|
|
45
|
+
sandbox.files.write('/home/user/hello.txt', 'Hello, World!')
|
|
46
|
+
content = sandbox.files.read('/home/user/hello.txt')
|
|
47
|
+
|
|
48
|
+
# List directory (returns EntryInfo objects)
|
|
49
|
+
entries = sandbox.files.list('/home/user')
|
|
50
|
+
entries.each { |e| puts "#{e.name} (#{e.type}, #{e.size} bytes)" }
|
|
51
|
+
|
|
52
|
+
# Background commands
|
|
53
|
+
handle = sandbox.commands.run("sleep 30", background: true)
|
|
54
|
+
handle.kill
|
|
55
|
+
|
|
56
|
+
# PTY (interactive terminal)
|
|
57
|
+
handle = sandbox.pty.create
|
|
58
|
+
handle.send_stdin("ls -la\n")
|
|
59
|
+
handle.kill
|
|
60
|
+
|
|
61
|
+
# Git operations
|
|
62
|
+
sandbox.git.clone("https://github.com/user/repo.git", path: "/home/user/repo")
|
|
63
|
+
status = sandbox.git.status("/home/user/repo")
|
|
64
|
+
puts "Branch: #{status.current_branch}, Clean: #{status.clean?}"
|
|
65
|
+
|
|
66
|
+
# Sandbox lifecycle
|
|
67
|
+
sandbox.set_timeout(600) # extend by 10 minutes
|
|
68
|
+
sandbox.pause
|
|
69
|
+
sandbox.resume(timeout: 300)
|
|
70
|
+
|
|
71
|
+
# Clean up
|
|
72
|
+
sandbox.kill
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Option 1: Environment variable (recommended)
|
|
79
|
+
export E2B_API_KEY=your-api-key
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Option 2: Global configuration
|
|
84
|
+
E2B.configure do |config|
|
|
85
|
+
config.api_key = 'your-api-key'
|
|
86
|
+
config.domain = 'e2b.app'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Option 3: Per-call
|
|
90
|
+
sandbox = E2B::Sandbox.create(template: "base", api_key: "your-key")
|
|
91
|
+
|
|
92
|
+
# Option 4: Client class (backward compatible)
|
|
93
|
+
client = E2B::Client.new(api_key: "your-key")
|
|
94
|
+
sandbox = client.create(template: "base")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API Reference
|
|
98
|
+
|
|
99
|
+
### Sandbox (class methods)
|
|
100
|
+
|
|
101
|
+
| Method | Description |
|
|
102
|
+
|--------|-------------|
|
|
103
|
+
| `Sandbox.create(template:, timeout:, metadata:, envs:, api_key:)` | Create a new sandbox |
|
|
104
|
+
| `Sandbox.connect(sandbox_id, timeout:, api_key:)` | Connect to existing sandbox |
|
|
105
|
+
| `Sandbox.list(query:, limit:, api_key:)` | List running sandboxes |
|
|
106
|
+
| `Sandbox.kill(sandbox_id, api_key:)` | Kill a sandbox by ID |
|
|
107
|
+
|
|
108
|
+
### Sandbox (instance)
|
|
109
|
+
|
|
110
|
+
| Method | Description |
|
|
111
|
+
|--------|-------------|
|
|
112
|
+
| `sandbox.commands` | Command execution service |
|
|
113
|
+
| `sandbox.files` | Filesystem service |
|
|
114
|
+
| `sandbox.pty` | PTY (terminal) service |
|
|
115
|
+
| `sandbox.git` | Git operations service |
|
|
116
|
+
| `sandbox.set_timeout(seconds)` | Extend sandbox lifetime |
|
|
117
|
+
| `sandbox.get_host(port)` | Get host string for a port |
|
|
118
|
+
| `sandbox.get_url(port)` | Get full URL for a port |
|
|
119
|
+
| `sandbox.pause` / `sandbox.resume` | Pause/resume sandbox |
|
|
120
|
+
| `sandbox.create_snapshot` | Create sandbox snapshot |
|
|
121
|
+
| `sandbox.kill` | Terminate sandbox |
|
|
122
|
+
|
|
123
|
+
### Commands (`sandbox.commands`)
|
|
124
|
+
|
|
125
|
+
| Method | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `run(cmd, background:, envs:, cwd:, timeout:, on_stdout:, on_stderr:)` | Run command (returns `CommandResult` or `CommandHandle`) |
|
|
128
|
+
| `list` | List running processes |
|
|
129
|
+
| `kill(pid)` | Kill a process |
|
|
130
|
+
| `send_stdin(pid, data)` | Send stdin to a process |
|
|
131
|
+
| `connect(pid)` | Connect to running process |
|
|
132
|
+
|
|
133
|
+
### Filesystem (`sandbox.files`)
|
|
134
|
+
|
|
135
|
+
| Method | Description |
|
|
136
|
+
|--------|-------------|
|
|
137
|
+
| `read(path)` | Read file content |
|
|
138
|
+
| `write(path, data)` | Write file (via REST upload) |
|
|
139
|
+
| `write_files(files)` | Write multiple files |
|
|
140
|
+
| `list(path, depth:)` | List directory (returns `EntryInfo[]`) |
|
|
141
|
+
| `get_info(path)` | Get file/dir info (returns `EntryInfo`) |
|
|
142
|
+
| `exists?(path)` | Check if path exists |
|
|
143
|
+
| `make_dir(path)` | Create directory |
|
|
144
|
+
| `remove(path)` | Remove file/directory |
|
|
145
|
+
| `rename(old_path, new_path)` | Rename/move |
|
|
146
|
+
| `watch_dir(path, recursive:)` | Watch directory (returns `WatchHandle`) |
|
|
147
|
+
|
|
148
|
+
### PTY (`sandbox.pty`)
|
|
149
|
+
|
|
150
|
+
| Method | Description |
|
|
151
|
+
|--------|-------------|
|
|
152
|
+
| `create(size:, cwd:, envs:)` | Create PTY session (returns `CommandHandle`) |
|
|
153
|
+
| `connect(pid)` | Connect to existing PTY |
|
|
154
|
+
| `send_stdin(pid, data)` | Send input to PTY |
|
|
155
|
+
| `kill(pid)` | Kill PTY process |
|
|
156
|
+
| `resize(pid, size)` | Resize terminal |
|
|
157
|
+
| `close_stdin(pid)` | Close PTY stdin (send EOF) |
|
|
158
|
+
| `list` | List running processes |
|
|
159
|
+
|
|
160
|
+
### Git (`sandbox.git`)
|
|
161
|
+
|
|
162
|
+
| Method | Description |
|
|
163
|
+
|--------|-------------|
|
|
164
|
+
| `clone(url, path:, branch:, depth:, username:, password:)` | Clone repository |
|
|
165
|
+
| `init(path, bare:, initial_branch:)` | Initialize repository |
|
|
166
|
+
| `status(path)` | Get repo status (returns `GitStatus`) |
|
|
167
|
+
| `branches(path)` | List branches (returns `GitBranches`) |
|
|
168
|
+
| `add(path, files:, all:)` | Stage files |
|
|
169
|
+
| `commit(path, message, author_name:, author_email:)` | Create commit |
|
|
170
|
+
| `push(path, remote:, branch:, username:, password:)` | Push to remote |
|
|
171
|
+
| `pull(path, remote:, branch:, username:, password:)` | Pull from remote |
|
|
172
|
+
| `create_branch` / `checkout_branch` / `delete_branch` | Branch management |
|
|
173
|
+
| `remote_add(path, name, url)` / `remote_get(path, name)` | Remote management |
|
|
174
|
+
| `reset(path, mode:, target:)` / `restore(path, paths)` | Reset/restore changes |
|
|
175
|
+
| `set_config` / `get_config` | Git configuration |
|
|
176
|
+
| `configure_user(name, email)` | Set user name/email |
|
|
177
|
+
| `dangerously_authenticate(username, password)` | Store credentials globally |
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/multipart"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module E2B
|
|
8
|
+
module API
|
|
9
|
+
# HTTP client wrapper for E2B API communication
|
|
10
|
+
#
|
|
11
|
+
# Handles authentication, request/response processing, and error handling.
|
|
12
|
+
class HttpClient
|
|
13
|
+
# Default request timeout in seconds
|
|
14
|
+
DEFAULT_TIMEOUT = 120
|
|
15
|
+
|
|
16
|
+
# @return [String] Base URL for API requests
|
|
17
|
+
attr_reader :base_url
|
|
18
|
+
|
|
19
|
+
# Initialize a new HTTP client
|
|
20
|
+
#
|
|
21
|
+
# @param base_url [String] Base URL for API requests
|
|
22
|
+
# @param api_key [String] API key for authentication
|
|
23
|
+
# @param logger [Logger, nil] Optional logger
|
|
24
|
+
def initialize(base_url:, api_key:, logger: nil)
|
|
25
|
+
@base_url = base_url.end_with?("/") ? base_url : "#{base_url}/"
|
|
26
|
+
@api_key = api_key
|
|
27
|
+
@logger = logger
|
|
28
|
+
@connection = build_connection
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Perform a GET request
|
|
32
|
+
#
|
|
33
|
+
# @param path [String] API endpoint path
|
|
34
|
+
# @param params [Hash] Query parameters
|
|
35
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
36
|
+
# @return [Hash, Array, String] Parsed response body
|
|
37
|
+
def get(path, params: {}, timeout: DEFAULT_TIMEOUT)
|
|
38
|
+
handle_response do
|
|
39
|
+
@connection.get(normalize_path(path)) do |req|
|
|
40
|
+
req.params = params
|
|
41
|
+
req.options.timeout = timeout
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Perform a POST request
|
|
47
|
+
#
|
|
48
|
+
# @param path [String] API endpoint path
|
|
49
|
+
# @param body [Hash, nil] Request body
|
|
50
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
51
|
+
# @return [Hash, Array, String] Parsed response body
|
|
52
|
+
def post(path, body: nil, timeout: DEFAULT_TIMEOUT)
|
|
53
|
+
handle_response do
|
|
54
|
+
@connection.post(normalize_path(path)) do |req|
|
|
55
|
+
req.body = body.to_json if body
|
|
56
|
+
req.options.timeout = timeout
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Perform a PUT request
|
|
62
|
+
#
|
|
63
|
+
# @param path [String] API endpoint path
|
|
64
|
+
# @param body [Hash, nil] Request body
|
|
65
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
66
|
+
# @return [Hash, Array, String] Parsed response body
|
|
67
|
+
def put(path, body: nil, timeout: DEFAULT_TIMEOUT)
|
|
68
|
+
handle_response do
|
|
69
|
+
@connection.put(normalize_path(path)) do |req|
|
|
70
|
+
req.body = body.to_json if body
|
|
71
|
+
req.options.timeout = timeout
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Perform a DELETE request
|
|
77
|
+
#
|
|
78
|
+
# @param path [String] API endpoint path
|
|
79
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
80
|
+
# @return [Hash, Array, String, nil] Parsed response body
|
|
81
|
+
def delete(path, timeout: DEFAULT_TIMEOUT)
|
|
82
|
+
handle_response do
|
|
83
|
+
@connection.delete(normalize_path(path)) do |req|
|
|
84
|
+
req.options.timeout = timeout
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def normalize_path(path)
|
|
92
|
+
path.to_s.sub(%r{^/+}, "")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_connection
|
|
96
|
+
ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
|
|
97
|
+
|
|
98
|
+
Faraday.new(url: @base_url, ssl: { verify: ssl_verify }) do |conn|
|
|
99
|
+
conn.request :json
|
|
100
|
+
conn.response :json, content_type: /\bjson$/
|
|
101
|
+
conn.adapter Faraday.default_adapter
|
|
102
|
+
|
|
103
|
+
conn.headers["X-API-Key"] = @api_key
|
|
104
|
+
conn.headers["Content-Type"] = "application/json"
|
|
105
|
+
conn.headers["Accept"] = "application/json"
|
|
106
|
+
conn.headers["User-Agent"] = "e2b-ruby-sdk/#{E2B::VERSION}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def handle_response
|
|
111
|
+
response = yield
|
|
112
|
+
handle_error(response) unless response.success?
|
|
113
|
+
|
|
114
|
+
body = response.body
|
|
115
|
+
|
|
116
|
+
# If body is a string but should be JSON, try to parse it
|
|
117
|
+
if body.is_a?(String) && !body.empty?
|
|
118
|
+
content_type = response.headers["content-type"] rescue "unknown"
|
|
119
|
+
if content_type&.include?("json") || body.start_with?("{", "[")
|
|
120
|
+
begin
|
|
121
|
+
return JSON.parse(body)
|
|
122
|
+
rescue JSON::ParserError
|
|
123
|
+
# Return as-is if can't parse
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
body
|
|
129
|
+
rescue Faraday::TimeoutError => e
|
|
130
|
+
raise E2B::TimeoutError, "Request timed out: #{e.message}"
|
|
131
|
+
rescue Faraday::ConnectionFailed => e
|
|
132
|
+
raise E2B::E2BError, "Connection failed: #{e.message}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def handle_error(response)
|
|
136
|
+
message = extract_error_message(response)
|
|
137
|
+
status = response.status
|
|
138
|
+
headers = response.headers.to_h
|
|
139
|
+
|
|
140
|
+
case status
|
|
141
|
+
when 401, 403
|
|
142
|
+
raise E2B::AuthenticationError.new(message, status_code: status, headers: headers)
|
|
143
|
+
when 404
|
|
144
|
+
raise E2B::NotFoundError.new(message, status_code: status, headers: headers)
|
|
145
|
+
when 409
|
|
146
|
+
raise E2B::ConflictError.new(message, status_code: status, headers: headers)
|
|
147
|
+
when 429
|
|
148
|
+
raise E2B::RateLimitError.new(message, status_code: status, headers: headers)
|
|
149
|
+
else
|
|
150
|
+
raise E2B::E2BError.new(message, status_code: status, headers: headers)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def extract_error_message(response)
|
|
155
|
+
body = response.body
|
|
156
|
+
return body["message"] if body.is_a?(Hash) && body["message"]
|
|
157
|
+
return body["error"] if body.is_a?(Hash) && body["error"]
|
|
158
|
+
return body.to_s if body.is_a?(String) && !body.empty?
|
|
159
|
+
|
|
160
|
+
"HTTP #{response.status} error"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
data/lib/e2b/client.rb
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E2B
|
|
4
|
+
# Client for interacting with the E2B API
|
|
5
|
+
#
|
|
6
|
+
# This class provides a convenient wrapper around {Sandbox} class methods.
|
|
7
|
+
# For the most direct API (matching the official SDK pattern), use
|
|
8
|
+
# {Sandbox.create}, {Sandbox.connect}, etc. directly.
|
|
9
|
+
#
|
|
10
|
+
# @example Using Client
|
|
11
|
+
# client = E2B::Client.new(api_key: "your-api-key")
|
|
12
|
+
# sandbox = client.create(template: "base")
|
|
13
|
+
#
|
|
14
|
+
# @example Using Sandbox directly (recommended, matches official SDK)
|
|
15
|
+
# sandbox = E2B::Sandbox.create(template: "base", api_key: "your-key")
|
|
16
|
+
class Client
|
|
17
|
+
# @return [Configuration] Client configuration
|
|
18
|
+
attr_reader :config
|
|
19
|
+
|
|
20
|
+
# Initialize a new E2B client
|
|
21
|
+
#
|
|
22
|
+
# @param config_or_options [Configuration, Hash, nil] Configuration or options hash
|
|
23
|
+
# @option config_or_options [String] :api_key API key for authentication
|
|
24
|
+
# @option config_or_options [String] :api_url API URL
|
|
25
|
+
# @option config_or_options [Integer] :timeout_ms Request timeout in milliseconds
|
|
26
|
+
def initialize(config_or_options = nil)
|
|
27
|
+
@config = resolve_config(config_or_options)
|
|
28
|
+
@config.validate!
|
|
29
|
+
|
|
30
|
+
@http_client = API::HttpClient.new(
|
|
31
|
+
base_url: @config.api_url,
|
|
32
|
+
api_key: @config.api_key,
|
|
33
|
+
logger: @config.logger
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@domain = Sandbox::DEFAULT_DOMAIN
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Create a new sandbox
|
|
40
|
+
#
|
|
41
|
+
# @param template [String] Template ID or alias
|
|
42
|
+
# @param timeout [Integer, nil] Sandbox timeout in seconds (or timeout_ms in milliseconds for backward compat)
|
|
43
|
+
# @param timeout_ms [Integer, nil] Sandbox timeout in milliseconds (deprecated, use timeout in seconds)
|
|
44
|
+
# @param metadata [Hash, nil] Custom metadata
|
|
45
|
+
# @param envs [Hash{String => String}, nil] Environment variables
|
|
46
|
+
# @return [Sandbox] The created sandbox instance
|
|
47
|
+
def create(template: "base", timeout: nil, timeout_ms: nil, metadata: nil, envs: nil, **_opts)
|
|
48
|
+
# Support both seconds and milliseconds for backward compat
|
|
49
|
+
timeout_seconds = if timeout
|
|
50
|
+
timeout
|
|
51
|
+
elsif timeout_ms
|
|
52
|
+
(timeout_ms / 1000).to_i
|
|
53
|
+
else
|
|
54
|
+
(@config.sandbox_timeout_ms / 1000).to_i
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
body = {
|
|
58
|
+
templateID: template,
|
|
59
|
+
timeout: timeout_seconds
|
|
60
|
+
}
|
|
61
|
+
body[:metadata] = metadata if metadata
|
|
62
|
+
body[:envVars] = envs if envs
|
|
63
|
+
|
|
64
|
+
response = @http_client.post("/sandboxes", body: body, timeout: 120)
|
|
65
|
+
|
|
66
|
+
Sandbox.new(
|
|
67
|
+
sandbox_data: response,
|
|
68
|
+
http_client: @http_client,
|
|
69
|
+
api_key: @config.api_key,
|
|
70
|
+
domain: @domain
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Connect to an existing sandbox
|
|
75
|
+
#
|
|
76
|
+
# @param sandbox_id [String] The sandbox ID
|
|
77
|
+
# @param timeout [Integer, nil] Timeout in seconds
|
|
78
|
+
# @return [Sandbox]
|
|
79
|
+
def connect(sandbox_id, timeout: nil)
|
|
80
|
+
if timeout
|
|
81
|
+
response = @http_client.post("/sandboxes/#{sandbox_id}/connect",
|
|
82
|
+
body: { timeout: timeout })
|
|
83
|
+
else
|
|
84
|
+
response = @http_client.get("/sandboxes/#{sandbox_id}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Sandbox.new(
|
|
88
|
+
sandbox_data: response,
|
|
89
|
+
http_client: @http_client,
|
|
90
|
+
api_key: @config.api_key,
|
|
91
|
+
domain: @domain
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Get sandbox details
|
|
96
|
+
#
|
|
97
|
+
# @param sandbox_id [String] The sandbox ID
|
|
98
|
+
# @return [Sandbox]
|
|
99
|
+
def get(sandbox_id)
|
|
100
|
+
response = @http_client.get("/sandboxes/#{sandbox_id}")
|
|
101
|
+
|
|
102
|
+
Sandbox.new(
|
|
103
|
+
sandbox_data: response,
|
|
104
|
+
http_client: @http_client,
|
|
105
|
+
api_key: @config.api_key,
|
|
106
|
+
domain: @domain
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# List sandboxes
|
|
111
|
+
#
|
|
112
|
+
# @param metadata [Hash, nil] Filter by metadata
|
|
113
|
+
# @param state [String, nil] Filter by state
|
|
114
|
+
# @param limit [Integer] Maximum results
|
|
115
|
+
# @return [Array<Sandbox>]
|
|
116
|
+
def list(metadata: nil, state: nil, limit: 100)
|
|
117
|
+
params = { limit: limit }
|
|
118
|
+
params[:metadata] = metadata.to_json if metadata
|
|
119
|
+
params[:state] = state if state
|
|
120
|
+
|
|
121
|
+
response = @http_client.get("/v2/sandboxes", params: params)
|
|
122
|
+
|
|
123
|
+
sandboxes = if response.is_a?(Array)
|
|
124
|
+
response
|
|
125
|
+
elsif response.is_a?(Hash)
|
|
126
|
+
response["sandboxes"] || response[:sandboxes] || []
|
|
127
|
+
else
|
|
128
|
+
[]
|
|
129
|
+
end
|
|
130
|
+
Array(sandboxes).map do |sandbox_data|
|
|
131
|
+
Sandbox.new(
|
|
132
|
+
sandbox_data: sandbox_data,
|
|
133
|
+
http_client: @http_client,
|
|
134
|
+
api_key: @config.api_key,
|
|
135
|
+
domain: @domain
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Kill a sandbox
|
|
141
|
+
#
|
|
142
|
+
# @param sandbox_id [String] The sandbox ID
|
|
143
|
+
# @return [Boolean]
|
|
144
|
+
def kill(sandbox_id)
|
|
145
|
+
@http_client.delete("/sandboxes/#{sandbox_id}")
|
|
146
|
+
true
|
|
147
|
+
rescue NotFoundError
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Set sandbox timeout
|
|
152
|
+
#
|
|
153
|
+
# @param sandbox_id [String] The sandbox ID
|
|
154
|
+
# @param timeout [Integer] Timeout in seconds
|
|
155
|
+
def set_timeout(sandbox_id, timeout)
|
|
156
|
+
@http_client.post("/sandboxes/#{sandbox_id}/timeout",
|
|
157
|
+
body: { timeout: timeout })
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Pause a sandbox
|
|
161
|
+
#
|
|
162
|
+
# @param sandbox_id [String] The sandbox ID
|
|
163
|
+
def pause(sandbox_id)
|
|
164
|
+
@http_client.post("/sandboxes/#{sandbox_id}/pause")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Resume a sandbox
|
|
168
|
+
#
|
|
169
|
+
# @param sandbox_id [String] The sandbox ID
|
|
170
|
+
# @param timeout [Integer, nil] New timeout in seconds
|
|
171
|
+
# @return [Sandbox]
|
|
172
|
+
def resume(sandbox_id, timeout: nil)
|
|
173
|
+
body = {}
|
|
174
|
+
body[:timeout] = timeout if timeout
|
|
175
|
+
|
|
176
|
+
response = @http_client.post("/sandboxes/#{sandbox_id}/connect", body: body)
|
|
177
|
+
|
|
178
|
+
Sandbox.new(
|
|
179
|
+
sandbox_data: response,
|
|
180
|
+
http_client: @http_client,
|
|
181
|
+
api_key: @config.api_key,
|
|
182
|
+
domain: @domain
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def resolve_config(config_or_options)
|
|
189
|
+
case config_or_options
|
|
190
|
+
when Configuration
|
|
191
|
+
config_or_options
|
|
192
|
+
when Hash
|
|
193
|
+
Configuration.new(**config_or_options)
|
|
194
|
+
when nil
|
|
195
|
+
E2B.configuration || Configuration.new
|
|
196
|
+
else
|
|
197
|
+
raise ArgumentError, "Expected Configuration, Hash, or nil"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E2B
|
|
4
|
+
# Configuration for E2B SDK
|
|
5
|
+
#
|
|
6
|
+
# Supports environment variables:
|
|
7
|
+
# - E2B_API_KEY: API key for authentication
|
|
8
|
+
# - E2B_ACCESS_TOKEN: Access token (alternative auth)
|
|
9
|
+
# - E2B_DOMAIN: Custom domain (default: e2b.app)
|
|
10
|
+
# - E2B_API_URL: Custom API URL
|
|
11
|
+
# - E2B_DEBUG: Enable debug logging
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# E2B.configure do |config|
|
|
15
|
+
# config.api_key = "your-api-key"
|
|
16
|
+
# config.request_timeout = 120
|
|
17
|
+
# end
|
|
18
|
+
class Configuration
|
|
19
|
+
# Default API base URL
|
|
20
|
+
DEFAULT_API_URL = "https://api.e2b.app"
|
|
21
|
+
|
|
22
|
+
# Default domain
|
|
23
|
+
DEFAULT_DOMAIN = "e2b.app"
|
|
24
|
+
|
|
25
|
+
# Default request timeout in seconds
|
|
26
|
+
DEFAULT_REQUEST_TIMEOUT = 60
|
|
27
|
+
|
|
28
|
+
# Default sandbox timeout in seconds
|
|
29
|
+
DEFAULT_SANDBOX_TIMEOUT = 300
|
|
30
|
+
|
|
31
|
+
# Default timeout in milliseconds (backward compat)
|
|
32
|
+
DEFAULT_TIMEOUT_MS = 300_000
|
|
33
|
+
|
|
34
|
+
# Default sandbox timeout in milliseconds (backward compat)
|
|
35
|
+
DEFAULT_SANDBOX_TIMEOUT_MS = 3_600_000
|
|
36
|
+
|
|
37
|
+
# Maximum sandbox timeout (24 hours for Pro)
|
|
38
|
+
MAX_SANDBOX_TIMEOUT_MS = 86_400_000
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] API key for authentication
|
|
41
|
+
attr_accessor :api_key
|
|
42
|
+
|
|
43
|
+
# @return [String, nil] Access token (alternative auth method)
|
|
44
|
+
attr_accessor :access_token
|
|
45
|
+
|
|
46
|
+
# @return [String] E2B domain
|
|
47
|
+
attr_accessor :domain
|
|
48
|
+
|
|
49
|
+
# @return [String] API base URL
|
|
50
|
+
attr_accessor :api_url
|
|
51
|
+
|
|
52
|
+
# @return [Integer] Request timeout in seconds
|
|
53
|
+
attr_accessor :request_timeout
|
|
54
|
+
|
|
55
|
+
# @return [Integer] Request timeout in milliseconds (backward compat)
|
|
56
|
+
attr_accessor :timeout_ms
|
|
57
|
+
|
|
58
|
+
# @return [Integer] Default sandbox timeout in milliseconds (backward compat)
|
|
59
|
+
attr_accessor :sandbox_timeout_ms
|
|
60
|
+
|
|
61
|
+
# @return [Boolean] Enable debug logging
|
|
62
|
+
attr_accessor :debug
|
|
63
|
+
|
|
64
|
+
# @return [String, nil] Default template ID
|
|
65
|
+
attr_accessor :default_template
|
|
66
|
+
|
|
67
|
+
# @return [Logger, nil] Optional logger
|
|
68
|
+
attr_accessor :logger
|
|
69
|
+
|
|
70
|
+
# Initialize configuration
|
|
71
|
+
#
|
|
72
|
+
# @param api_key [String, nil] API key (defaults to E2B_API_KEY env var)
|
|
73
|
+
# @param access_token [String, nil] Access token
|
|
74
|
+
# @param domain [String] E2B domain
|
|
75
|
+
# @param api_url [String] API base URL
|
|
76
|
+
# @param request_timeout [Integer] Request timeout in seconds
|
|
77
|
+
# @param timeout_ms [Integer] Request timeout in milliseconds (backward compat)
|
|
78
|
+
# @param sandbox_timeout_ms [Integer] Default sandbox timeout in milliseconds
|
|
79
|
+
# @param debug [Boolean] Enable debug logging
|
|
80
|
+
def initialize(
|
|
81
|
+
api_key: nil,
|
|
82
|
+
access_token: nil,
|
|
83
|
+
domain: nil,
|
|
84
|
+
api_url: nil,
|
|
85
|
+
request_timeout: DEFAULT_REQUEST_TIMEOUT,
|
|
86
|
+
timeout_ms: DEFAULT_TIMEOUT_MS,
|
|
87
|
+
sandbox_timeout_ms: DEFAULT_SANDBOX_TIMEOUT_MS,
|
|
88
|
+
debug: false
|
|
89
|
+
)
|
|
90
|
+
@api_key = api_key || ENV["E2B_API_KEY"]
|
|
91
|
+
@access_token = access_token || ENV["E2B_ACCESS_TOKEN"]
|
|
92
|
+
@domain = domain || ENV["E2B_DOMAIN"] || DEFAULT_DOMAIN
|
|
93
|
+
@api_url = api_url || ENV["E2B_API_URL"] || DEFAULT_API_URL
|
|
94
|
+
@request_timeout = request_timeout
|
|
95
|
+
@timeout_ms = timeout_ms
|
|
96
|
+
@sandbox_timeout_ms = sandbox_timeout_ms
|
|
97
|
+
@debug = debug || ENV["E2B_DEBUG"]&.downcase == "true"
|
|
98
|
+
@default_template = nil
|
|
99
|
+
@logger = nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Validate configuration
|
|
103
|
+
#
|
|
104
|
+
# @raise [ConfigurationError] If API key is missing
|
|
105
|
+
def validate!
|
|
106
|
+
if (@api_key.nil? || @api_key.empty?) && (@access_token.nil? || @access_token.empty?)
|
|
107
|
+
raise ConfigurationError,
|
|
108
|
+
"E2B API key is required. Set E2B_API_KEY environment variable or pass api_key option."
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check if configuration is valid
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean]
|
|
115
|
+
def valid?
|
|
116
|
+
(!@api_key.nil? && !@api_key.empty?) || (!@access_token.nil? && !@access_token.empty?)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|