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.
@@ -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