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
data/lib/e2b/sandbox.rb
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module E2B
|
|
7
|
+
# Represents an E2B Sandbox instance
|
|
8
|
+
#
|
|
9
|
+
# A Sandbox is an isolated cloud environment for running code, executing
|
|
10
|
+
# commands, and managing files securely. Create sandboxes using class methods
|
|
11
|
+
# or through {E2B::Client}.
|
|
12
|
+
#
|
|
13
|
+
# @example Create and use a sandbox
|
|
14
|
+
# sandbox = E2B::Sandbox.create(template: "base", api_key: "your-key")
|
|
15
|
+
#
|
|
16
|
+
# result = sandbox.commands.run("echo 'Hello'")
|
|
17
|
+
# puts result.stdout
|
|
18
|
+
#
|
|
19
|
+
# sandbox.files.write("/home/user/hello.txt", "Hello!")
|
|
20
|
+
# sandbox.kill
|
|
21
|
+
#
|
|
22
|
+
# @example Connect to an existing sandbox
|
|
23
|
+
# sandbox = E2B::Sandbox.connect("sandbox-id", api_key: "your-key")
|
|
24
|
+
# sandbox.commands.run("ls")
|
|
25
|
+
class Sandbox
|
|
26
|
+
# Default domain for E2B sandboxes
|
|
27
|
+
DEFAULT_DOMAIN = "e2b.app"
|
|
28
|
+
|
|
29
|
+
# Default sandbox timeout in seconds
|
|
30
|
+
DEFAULT_TIMEOUT = 300
|
|
31
|
+
|
|
32
|
+
# @return [String] Unique sandbox ID
|
|
33
|
+
attr_reader :sandbox_id
|
|
34
|
+
|
|
35
|
+
# @return [String] Template ID used to create this sandbox
|
|
36
|
+
attr_reader :template_id
|
|
37
|
+
|
|
38
|
+
# @return [String, nil] Sandbox alias/name
|
|
39
|
+
attr_reader :alias_name
|
|
40
|
+
|
|
41
|
+
# @return [String] Client ID
|
|
42
|
+
attr_reader :client_id
|
|
43
|
+
|
|
44
|
+
# @return [Time, nil] When the sandbox was started
|
|
45
|
+
attr_reader :started_at
|
|
46
|
+
|
|
47
|
+
# @return [Time, nil] When the sandbox will timeout
|
|
48
|
+
attr_reader :end_at
|
|
49
|
+
|
|
50
|
+
# @return [Integer, nil] CPU count
|
|
51
|
+
attr_reader :cpu_count
|
|
52
|
+
|
|
53
|
+
# @return [Integer, nil] Memory in MB
|
|
54
|
+
attr_reader :memory_mb
|
|
55
|
+
|
|
56
|
+
# @return [Hash] Metadata
|
|
57
|
+
attr_reader :metadata
|
|
58
|
+
|
|
59
|
+
# @return [String, nil] Access token for envd authentication
|
|
60
|
+
attr_reader :envd_access_token
|
|
61
|
+
|
|
62
|
+
# @return [Services::Commands] Command execution service
|
|
63
|
+
attr_reader :commands
|
|
64
|
+
|
|
65
|
+
# @return [Services::Filesystem] Filesystem service
|
|
66
|
+
attr_reader :files
|
|
67
|
+
|
|
68
|
+
# @return [Services::Pty] PTY (pseudo-terminal) service
|
|
69
|
+
attr_reader :pty
|
|
70
|
+
|
|
71
|
+
# @return [Services::Git] Git operations service
|
|
72
|
+
attr_reader :git
|
|
73
|
+
|
|
74
|
+
# -------------------------------------------------------------------
|
|
75
|
+
# Class methods (matching official SDK pattern)
|
|
76
|
+
# -------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
class << self
|
|
79
|
+
# Create a new sandbox
|
|
80
|
+
#
|
|
81
|
+
# @param template [String] Template ID or alias (default: "base")
|
|
82
|
+
# @param timeout [Integer] Sandbox timeout in seconds (default: 300)
|
|
83
|
+
# @param metadata [Hash, nil] Custom metadata key-value pairs
|
|
84
|
+
# @param envs [Hash{String => String}, nil] Environment variables
|
|
85
|
+
# @param api_key [String, nil] API key (defaults to E2B_API_KEY env var)
|
|
86
|
+
# @param domain [String] E2B domain
|
|
87
|
+
# @param request_timeout [Integer] HTTP request timeout in seconds
|
|
88
|
+
# @return [Sandbox] The created sandbox instance
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# sandbox = E2B::Sandbox.create(template: "base")
|
|
92
|
+
# sandbox = E2B::Sandbox.create(template: "python", timeout: 600)
|
|
93
|
+
def create(template: "base", timeout: DEFAULT_TIMEOUT, metadata: nil,
|
|
94
|
+
envs: nil, api_key: nil, domain: DEFAULT_DOMAIN,
|
|
95
|
+
request_timeout: 120)
|
|
96
|
+
api_key = resolve_api_key(api_key)
|
|
97
|
+
http_client = build_http_client(api_key)
|
|
98
|
+
|
|
99
|
+
body = {
|
|
100
|
+
templateID: template,
|
|
101
|
+
timeout: timeout
|
|
102
|
+
}
|
|
103
|
+
body[:metadata] = metadata if metadata
|
|
104
|
+
body[:envVars] = envs if envs
|
|
105
|
+
|
|
106
|
+
response = http_client.post("/sandboxes", body: body, timeout: request_timeout)
|
|
107
|
+
|
|
108
|
+
new(
|
|
109
|
+
sandbox_data: response,
|
|
110
|
+
http_client: http_client,
|
|
111
|
+
api_key: api_key,
|
|
112
|
+
domain: domain
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Connect to an existing running sandbox
|
|
117
|
+
#
|
|
118
|
+
# @param sandbox_id [String] The sandbox ID to connect to
|
|
119
|
+
# @param timeout [Integer, nil] New timeout in seconds (extends TTL)
|
|
120
|
+
# @param api_key [String, nil] API key
|
|
121
|
+
# @param domain [String] E2B domain
|
|
122
|
+
# @return [Sandbox] The sandbox instance
|
|
123
|
+
def connect(sandbox_id, timeout: nil, api_key: nil, domain: DEFAULT_DOMAIN)
|
|
124
|
+
api_key = resolve_api_key(api_key)
|
|
125
|
+
http_client = build_http_client(api_key)
|
|
126
|
+
|
|
127
|
+
if timeout
|
|
128
|
+
response = http_client.post("/sandboxes/#{sandbox_id}/connect",
|
|
129
|
+
body: { timeout: timeout })
|
|
130
|
+
else
|
|
131
|
+
response = http_client.get("/sandboxes/#{sandbox_id}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
new(
|
|
135
|
+
sandbox_data: response,
|
|
136
|
+
http_client: http_client,
|
|
137
|
+
api_key: api_key,
|
|
138
|
+
domain: domain
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# List running sandboxes
|
|
143
|
+
#
|
|
144
|
+
# @param query [Hash, nil] Filter parameters (metadata, state)
|
|
145
|
+
# @param limit [Integer] Maximum results per page
|
|
146
|
+
# @param next_token [String, nil] Pagination token
|
|
147
|
+
# @param api_key [String, nil] API key
|
|
148
|
+
# @return [Array<Hash>] List of sandbox info hashes
|
|
149
|
+
def list(query: nil, limit: 100, next_token: nil, api_key: nil)
|
|
150
|
+
api_key = resolve_api_key(api_key)
|
|
151
|
+
http_client = build_http_client(api_key)
|
|
152
|
+
|
|
153
|
+
params = { limit: limit }
|
|
154
|
+
params[:nextToken] = next_token if next_token
|
|
155
|
+
if query
|
|
156
|
+
params[:metadata] = query[:metadata].to_json if query[:metadata]
|
|
157
|
+
params[:state] = query[:state] if query[:state]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
response = http_client.get("/v2/sandboxes", params: params)
|
|
161
|
+
|
|
162
|
+
sandboxes = if response.is_a?(Array)
|
|
163
|
+
response
|
|
164
|
+
elsif response.is_a?(Hash)
|
|
165
|
+
response["sandboxes"] || response[:sandboxes] || []
|
|
166
|
+
else
|
|
167
|
+
[]
|
|
168
|
+
end
|
|
169
|
+
Array(sandboxes)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Kill a sandbox by ID
|
|
173
|
+
#
|
|
174
|
+
# @param sandbox_id [String] Sandbox ID to kill
|
|
175
|
+
# @param api_key [String, nil] API key
|
|
176
|
+
def kill(sandbox_id, api_key: nil)
|
|
177
|
+
api_key = resolve_api_key(api_key)
|
|
178
|
+
http_client = build_http_client(api_key)
|
|
179
|
+
http_client.delete("/sandboxes/#{sandbox_id}")
|
|
180
|
+
true
|
|
181
|
+
rescue E2B::NotFoundError
|
|
182
|
+
true
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def resolve_api_key(api_key)
|
|
188
|
+
key = api_key || E2B.configuration&.api_key || ENV["E2B_API_KEY"]
|
|
189
|
+
raise ConfigurationError, "E2B API key is required. Set E2B_API_KEY or pass api_key:" unless key && !key.empty?
|
|
190
|
+
key
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def build_http_client(api_key)
|
|
194
|
+
base_url = E2B.configuration&.api_url || Configuration::DEFAULT_API_URL
|
|
195
|
+
API::HttpClient.new(base_url: base_url, api_key: api_key)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# -------------------------------------------------------------------
|
|
200
|
+
# Instance methods
|
|
201
|
+
# -------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
# Initialize a new Sandbox instance
|
|
204
|
+
#
|
|
205
|
+
# @param sandbox_data [Hash] Sandbox data from API response
|
|
206
|
+
# @param http_client [API::HttpClient] HTTP client for API calls
|
|
207
|
+
# @param api_key [String] API key for authentication
|
|
208
|
+
# @param domain [String] E2B domain
|
|
209
|
+
def initialize(sandbox_data:, http_client:, api_key:, domain: DEFAULT_DOMAIN)
|
|
210
|
+
@http_client = http_client
|
|
211
|
+
@api_key = api_key
|
|
212
|
+
@domain = domain
|
|
213
|
+
|
|
214
|
+
process_sandbox_data(sandbox_data)
|
|
215
|
+
initialize_services
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Get sandbox info from the API
|
|
219
|
+
#
|
|
220
|
+
# @return [Hash] Sandbox info
|
|
221
|
+
def get_info
|
|
222
|
+
response = @http_client.get("/sandboxes/#{@sandbox_id}")
|
|
223
|
+
process_sandbox_data(response)
|
|
224
|
+
response
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Check if the sandbox is running
|
|
228
|
+
#
|
|
229
|
+
# @param request_timeout [Integer] Request timeout in seconds
|
|
230
|
+
# @return [Boolean]
|
|
231
|
+
def running?(request_timeout: 10)
|
|
232
|
+
return false if @end_at && Time.now >= @end_at
|
|
233
|
+
|
|
234
|
+
get_info
|
|
235
|
+
true
|
|
236
|
+
rescue NotFoundError, E2BError
|
|
237
|
+
false
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Set the sandbox timeout
|
|
241
|
+
#
|
|
242
|
+
# @param timeout [Integer] Timeout in seconds
|
|
243
|
+
def set_timeout(timeout)
|
|
244
|
+
raise ArgumentError, "Timeout must be positive" if timeout <= 0
|
|
245
|
+
raise ArgumentError, "Timeout cannot exceed 24 hours (86400s)" if timeout > 86_400
|
|
246
|
+
|
|
247
|
+
@http_client.post("/sandboxes/#{@sandbox_id}/timeout",
|
|
248
|
+
body: { timeout: timeout })
|
|
249
|
+
|
|
250
|
+
@end_at = Time.now + timeout
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Kill/terminate the sandbox
|
|
254
|
+
def kill
|
|
255
|
+
@http_client.delete("/sandboxes/#{@sandbox_id}")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Pause the sandbox (saves state for later resume)
|
|
259
|
+
def pause
|
|
260
|
+
@http_client.post("/sandboxes/#{@sandbox_id}/pause")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Resume a paused sandbox
|
|
264
|
+
#
|
|
265
|
+
# @param timeout [Integer, nil] New timeout in seconds
|
|
266
|
+
def resume(timeout: nil)
|
|
267
|
+
body = {}
|
|
268
|
+
body[:timeout] = timeout if timeout
|
|
269
|
+
|
|
270
|
+
response = @http_client.post("/sandboxes/#{@sandbox_id}/connect", body: body)
|
|
271
|
+
process_sandbox_data(response) if response.is_a?(Hash)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Create a snapshot of the sandbox
|
|
275
|
+
#
|
|
276
|
+
# @return [Hash] Snapshot info with snapshot_id
|
|
277
|
+
def create_snapshot
|
|
278
|
+
@http_client.post("/sandboxes/#{@sandbox_id}/snapshots")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Get the host string for a port (without protocol)
|
|
282
|
+
#
|
|
283
|
+
# @param port [Integer] Port number
|
|
284
|
+
# @return [String] Host string like "4321-abc123.e2b.app"
|
|
285
|
+
def get_host(port)
|
|
286
|
+
"#{port}-#{@sandbox_id}.#{@domain}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Get full URL for a port
|
|
290
|
+
#
|
|
291
|
+
# @param port [Integer] Port number
|
|
292
|
+
# @return [String] Full URL like "https://4321-abc123.e2b.app"
|
|
293
|
+
def get_url(port)
|
|
294
|
+
"https://#{get_host(port)}"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Get URL for downloading a file
|
|
298
|
+
#
|
|
299
|
+
# @param path [String] File path in the sandbox
|
|
300
|
+
# @param user [String, nil] Username context
|
|
301
|
+
# @return [String] Download URL
|
|
302
|
+
def download_url(path, user: nil)
|
|
303
|
+
encoded_path = URI.encode_www_form_component(path)
|
|
304
|
+
base = "https://#{Services::BaseService::ENVD_PORT}-#{@sandbox_id}.#{@domain}/files"
|
|
305
|
+
url = "#{base}?path=#{encoded_path}"
|
|
306
|
+
url += "&username=#{URI.encode_www_form_component(user)}" if user
|
|
307
|
+
url
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Get URL for uploading a file
|
|
311
|
+
#
|
|
312
|
+
# @param path [String, nil] Destination path
|
|
313
|
+
# @param user [String, nil] Username context
|
|
314
|
+
# @return [String] Upload URL
|
|
315
|
+
def upload_url(path = nil, user: nil)
|
|
316
|
+
base = "https://#{Services::BaseService::ENVD_PORT}-#{@sandbox_id}.#{@domain}/files"
|
|
317
|
+
params = []
|
|
318
|
+
params << "path=#{URI.encode_www_form_component(path)}" if path
|
|
319
|
+
params << "username=#{URI.encode_www_form_component(user)}" if user
|
|
320
|
+
params.empty? ? base : "#{base}?#{params.join("&")}"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Get sandbox metrics (CPU, memory, disk usage)
|
|
324
|
+
#
|
|
325
|
+
# @param start_time [Time, nil] Metrics start time
|
|
326
|
+
# @param end_time [Time, nil] Metrics end time
|
|
327
|
+
# @return [Array<Hash>] Metrics data
|
|
328
|
+
def get_metrics(start_time: nil, end_time: nil)
|
|
329
|
+
params = {}
|
|
330
|
+
params[:start] = start_time.iso8601 if start_time
|
|
331
|
+
params[:end] = end_time.iso8601 if end_time
|
|
332
|
+
|
|
333
|
+
@http_client.get("/sandboxes/#{@sandbox_id}/metrics", params: params)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Get sandbox logs
|
|
337
|
+
#
|
|
338
|
+
# @param start_time [Time, nil] Start time for logs
|
|
339
|
+
# @param limit [Integer] Maximum number of log entries
|
|
340
|
+
# @return [Array<Hash>] Log entries
|
|
341
|
+
def logs(start_time: nil, limit: 100)
|
|
342
|
+
params = { limit: limit }
|
|
343
|
+
params[:start] = start_time.iso8601 if start_time
|
|
344
|
+
|
|
345
|
+
response = @http_client.get("/sandboxes/#{@sandbox_id}/logs", params: params)
|
|
346
|
+
response.is_a?(Hash) ? (response["logs"] || []) : response
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Time remaining until sandbox timeout
|
|
350
|
+
#
|
|
351
|
+
# @return [Integer] Seconds remaining, 0 if expired or unknown
|
|
352
|
+
def time_remaining
|
|
353
|
+
return 0 if @end_at.nil?
|
|
354
|
+
|
|
355
|
+
remaining = (@end_at - Time.now).to_i
|
|
356
|
+
remaining.positive? ? remaining : 0
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Alias for sandbox_id
|
|
360
|
+
alias id sandbox_id
|
|
361
|
+
|
|
362
|
+
private
|
|
363
|
+
|
|
364
|
+
def process_sandbox_data(data)
|
|
365
|
+
return unless data.is_a?(Hash)
|
|
366
|
+
|
|
367
|
+
@sandbox_id = data["sandboxID"] || data["sandbox_id"] || data[:sandboxID] || @sandbox_id
|
|
368
|
+
@template_id = data["templateID"] || data["template_id"] || data[:templateID] || @template_id
|
|
369
|
+
@alias_name = data["alias"] || data[:alias]
|
|
370
|
+
@client_id = data["clientID"] || data["client_id"] || data[:clientID]
|
|
371
|
+
@cpu_count = data["cpuCount"] || data["cpu_count"] || data[:cpuCount]
|
|
372
|
+
@memory_mb = data["memoryMB"] || data["memory_mb"] || data[:memoryMB]
|
|
373
|
+
@metadata = data["metadata"] || data[:metadata] || {}
|
|
374
|
+
|
|
375
|
+
@envd_access_token = data["envdAccessToken"] || data["envd_access_token"] || data[:envdAccessToken] || @envd_access_token
|
|
376
|
+
|
|
377
|
+
@started_at = parse_time(data["startedAt"] || data["started_at"] || data[:startedAt])
|
|
378
|
+
@end_at = parse_time(data["endAt"] || data["end_at"] || data[:endAt])
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def initialize_services
|
|
382
|
+
service_opts = {
|
|
383
|
+
sandbox_id: @sandbox_id,
|
|
384
|
+
sandbox_domain: @domain,
|
|
385
|
+
api_key: @api_key,
|
|
386
|
+
access_token: @envd_access_token
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@commands = Services::Commands.new(**service_opts)
|
|
390
|
+
|
|
391
|
+
@files = Services::Filesystem.new(**service_opts)
|
|
392
|
+
|
|
393
|
+
@pty = Services::Pty.new(**service_opts)
|
|
394
|
+
|
|
395
|
+
@git = Services::Git.new(commands: @commands)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def parse_time(value)
|
|
399
|
+
return nil if value.nil?
|
|
400
|
+
return value if value.is_a?(Time)
|
|
401
|
+
|
|
402
|
+
Time.parse(value)
|
|
403
|
+
rescue ArgumentError
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|