e2b 0.2.0 → 0.3.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 +4 -4
- data/README.md +6 -2
- data/lib/e2b/api/http_client.rb +30 -19
- data/lib/e2b/client.rb +79 -36
- data/lib/e2b/configuration.rb +12 -6
- data/lib/e2b/dockerfile_parser.rb +179 -0
- data/lib/e2b/errors.rb +24 -1
- data/lib/e2b/models/build_info.rb +29 -0
- data/lib/e2b/models/build_status_reason.rb +27 -0
- data/lib/e2b/models/sandbox_info.rb +19 -2
- data/lib/e2b/models/snapshot_info.rb +19 -0
- data/lib/e2b/models/template_build_status_response.rb +31 -0
- data/lib/e2b/models/template_log_entry.rb +54 -0
- data/lib/e2b/models/template_tag.rb +34 -0
- data/lib/e2b/models/template_tag_info.rb +21 -0
- data/lib/e2b/paginator.rb +97 -0
- data/lib/e2b/ready_cmd.rb +36 -0
- data/lib/e2b/sandbox.rb +217 -66
- data/lib/e2b/sandbox_helpers.rb +100 -0
- data/lib/e2b/services/base_service.rb +64 -15
- data/lib/e2b/services/command_handle.rb +189 -36
- data/lib/e2b/services/commands.rb +37 -50
- data/lib/e2b/services/filesystem.rb +70 -23
- data/lib/e2b/services/live_streamable.rb +94 -0
- data/lib/e2b/services/pty.rb +13 -64
- data/lib/e2b/services/watch_handle.rb +6 -3
- data/lib/e2b/template.rb +1089 -0
- data/lib/e2b/template_logger.rb +52 -0
- data/lib/e2b/version.rb +1 -1
- data/lib/e2b.rb +16 -0
- metadata +44 -2
data/lib/e2b/sandbox.rb
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
4
|
require "securerandom"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "digest"
|
|
7
|
+
require "json"
|
|
8
|
+
require "rubygems/version"
|
|
5
9
|
|
|
6
10
|
module E2B
|
|
7
11
|
# Represents an E2B Sandbox instance
|
|
@@ -29,6 +33,12 @@ module E2B
|
|
|
29
33
|
# Default sandbox timeout in seconds
|
|
30
34
|
DEFAULT_TIMEOUT = 300
|
|
31
35
|
|
|
36
|
+
# Default template used when enabling MCP without an explicit template.
|
|
37
|
+
DEFAULT_MCP_TEMPLATE = "mcp-gateway"
|
|
38
|
+
|
|
39
|
+
# MCP gateway port.
|
|
40
|
+
MCP_PORT = 50005
|
|
41
|
+
|
|
32
42
|
# @return [String] Unique sandbox ID
|
|
33
43
|
attr_reader :sandbox_id
|
|
34
44
|
|
|
@@ -56,9 +66,18 @@ module E2B
|
|
|
56
66
|
# @return [Hash] Metadata
|
|
57
67
|
attr_reader :metadata
|
|
58
68
|
|
|
69
|
+
# @return [String, nil] Current sandbox state
|
|
70
|
+
attr_reader :state
|
|
71
|
+
|
|
72
|
+
# @return [String, nil] Envd version reported by the control plane
|
|
73
|
+
attr_reader :envd_version
|
|
74
|
+
|
|
59
75
|
# @return [String, nil] Access token for envd authentication
|
|
60
76
|
attr_reader :envd_access_token
|
|
61
77
|
|
|
78
|
+
# @return [String, nil] Access token required for proxied public traffic
|
|
79
|
+
attr_reader :traffic_access_token
|
|
80
|
+
|
|
62
81
|
# @return [Services::Commands] Command execution service
|
|
63
82
|
attr_reader :commands
|
|
64
83
|
|
|
@@ -76,6 +95,8 @@ module E2B
|
|
|
76
95
|
# -------------------------------------------------------------------
|
|
77
96
|
|
|
78
97
|
class << self
|
|
98
|
+
include SandboxHelpers
|
|
99
|
+
|
|
79
100
|
# Create a new sandbox
|
|
80
101
|
#
|
|
81
102
|
# @param template [String] Template ID or alias (default: "base")
|
|
@@ -91,26 +112,44 @@ module E2B
|
|
|
91
112
|
# sandbox = E2B::Sandbox.create(template: "base")
|
|
92
113
|
# sandbox = E2B::Sandbox.create(template: "python", timeout: 600)
|
|
93
114
|
def create(template: "base", timeout: DEFAULT_TIMEOUT, metadata: nil,
|
|
94
|
-
envs: nil,
|
|
115
|
+
envs: nil, secure: true, allow_internet_access: true,
|
|
116
|
+
network: nil, lifecycle: nil, auto_pause: nil, mcp: nil,
|
|
117
|
+
api_key: nil, access_token: nil, domain: nil,
|
|
95
118
|
request_timeout: 120)
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
credentials = resolve_credentials(api_key: api_key, access_token: access_token)
|
|
120
|
+
domain = resolve_domain(domain)
|
|
121
|
+
http_client = build_http_client(**credentials, domain: domain)
|
|
122
|
+
template = resolved_template(template, mcp: mcp)
|
|
123
|
+
lifecycle = normalized_lifecycle(lifecycle: lifecycle, auto_pause: auto_pause)
|
|
98
124
|
|
|
99
125
|
body = {
|
|
100
126
|
templateID: template,
|
|
101
|
-
timeout: timeout
|
|
127
|
+
timeout: timeout,
|
|
128
|
+
secure: secure,
|
|
129
|
+
allow_internet_access: allow_internet_access,
|
|
130
|
+
autoPause: lifecycle[:on_timeout] == "pause"
|
|
102
131
|
}
|
|
103
132
|
body[:metadata] = metadata if metadata
|
|
104
133
|
body[:envVars] = envs if envs
|
|
134
|
+
body[:mcp] = mcp if mcp
|
|
135
|
+
body[:network] = network if network
|
|
136
|
+
if body[:autoPause]
|
|
137
|
+
body[:autoResume] = { enabled: lifecycle[:auto_resume] }
|
|
138
|
+
end
|
|
105
139
|
|
|
106
140
|
response = http_client.post("/sandboxes", body: body, timeout: request_timeout)
|
|
141
|
+
ensure_supported_envd_version!(response, http_client)
|
|
107
142
|
|
|
108
|
-
new(
|
|
143
|
+
sandbox = new(
|
|
109
144
|
sandbox_data: response,
|
|
110
145
|
http_client: http_client,
|
|
111
|
-
api_key: api_key,
|
|
146
|
+
api_key: credentials[:api_key],
|
|
112
147
|
domain: domain
|
|
113
148
|
)
|
|
149
|
+
|
|
150
|
+
start_mcp_gateway(sandbox, mcp) if mcp
|
|
151
|
+
|
|
152
|
+
sandbox
|
|
114
153
|
end
|
|
115
154
|
|
|
116
155
|
# Connect to an existing running sandbox
|
|
@@ -120,21 +159,18 @@ module E2B
|
|
|
120
159
|
# @param api_key [String, nil] API key
|
|
121
160
|
# @param domain [String] E2B domain
|
|
122
161
|
# @return [Sandbox] The sandbox instance
|
|
123
|
-
def connect(sandbox_id, timeout:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
else
|
|
131
|
-
response = http_client.get("/sandboxes/#{sandbox_id}")
|
|
132
|
-
end
|
|
162
|
+
def connect(sandbox_id, timeout: DEFAULT_TIMEOUT, api_key: nil, access_token: nil, domain: nil)
|
|
163
|
+
credentials = resolve_credentials(api_key: api_key, access_token: access_token)
|
|
164
|
+
domain = resolve_domain(domain)
|
|
165
|
+
http_client = build_http_client(**credentials, domain: domain)
|
|
166
|
+
|
|
167
|
+
response = http_client.post("/sandboxes/#{sandbox_id}/connect",
|
|
168
|
+
body: { timeout: timeout || DEFAULT_TIMEOUT })
|
|
133
169
|
|
|
134
170
|
new(
|
|
135
171
|
sandbox_data: response,
|
|
136
172
|
http_client: http_client,
|
|
137
|
-
api_key: api_key,
|
|
173
|
+
api_key: credentials[:api_key],
|
|
138
174
|
domain: domain
|
|
139
175
|
)
|
|
140
176
|
end
|
|
@@ -146,54 +182,62 @@ module E2B
|
|
|
146
182
|
# @param next_token [String, nil] Pagination token
|
|
147
183
|
# @param api_key [String, nil] API key
|
|
148
184
|
# @return [Array<Hash>] List of sandbox info hashes
|
|
149
|
-
def list(query: nil, limit: 100, next_token: nil, api_key: nil)
|
|
150
|
-
|
|
151
|
-
http_client = build_http_client(
|
|
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
|
|
185
|
+
def list(query: nil, limit: 100, next_token: nil, api_key: nil, access_token: nil, domain: nil)
|
|
186
|
+
credentials = resolve_credentials(api_key: api_key, access_token: access_token)
|
|
187
|
+
http_client = build_http_client(**credentials, domain: resolve_domain(domain))
|
|
159
188
|
|
|
160
|
-
|
|
189
|
+
SandboxPaginator.new(
|
|
190
|
+
http_client: http_client,
|
|
191
|
+
query: query,
|
|
192
|
+
limit: limit,
|
|
193
|
+
next_token: next_token
|
|
194
|
+
)
|
|
195
|
+
end
|
|
161
196
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
# List snapshots for the team, optionally filtered by source sandbox.
|
|
198
|
+
#
|
|
199
|
+
# @param sandbox_id [String, nil] Filter snapshots by source sandbox ID
|
|
200
|
+
# @param limit [Integer] Maximum results per page
|
|
201
|
+
# @param next_token [String, nil] Pagination token
|
|
202
|
+
# @return [SnapshotPaginator]
|
|
203
|
+
def list_snapshots(sandbox_id: nil, limit: 100, next_token: nil, api_key: nil, access_token: nil, domain: nil)
|
|
204
|
+
credentials = resolve_credentials(api_key: api_key, access_token: access_token)
|
|
205
|
+
http_client = build_http_client(**credentials, domain: resolve_domain(domain))
|
|
206
|
+
|
|
207
|
+
SnapshotPaginator.new(
|
|
208
|
+
http_client: http_client,
|
|
209
|
+
sandbox_id: sandbox_id,
|
|
210
|
+
limit: limit,
|
|
211
|
+
next_token: next_token
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Delete a snapshot template.
|
|
216
|
+
#
|
|
217
|
+
# @param snapshot_id [String] Snapshot identifier
|
|
218
|
+
# @return [Boolean] true if deleted, false if not found
|
|
219
|
+
def delete_snapshot(snapshot_id, api_key: nil, access_token: nil, domain: nil)
|
|
220
|
+
credentials = resolve_credentials(api_key: api_key, access_token: access_token)
|
|
221
|
+
http_client = build_http_client(**credentials, domain: resolve_domain(domain))
|
|
222
|
+
http_client.delete("/templates/#{snapshot_id}")
|
|
223
|
+
true
|
|
224
|
+
rescue E2B::NotFoundError
|
|
225
|
+
false
|
|
170
226
|
end
|
|
171
227
|
|
|
172
228
|
# Kill a sandbox by ID
|
|
173
229
|
#
|
|
174
230
|
# @param sandbox_id [String] Sandbox ID to kill
|
|
175
231
|
# @param api_key [String, nil] API key
|
|
176
|
-
def kill(sandbox_id, api_key: nil)
|
|
177
|
-
|
|
178
|
-
http_client = build_http_client(
|
|
232
|
+
def kill(sandbox_id, api_key: nil, access_token: nil, domain: nil)
|
|
233
|
+
credentials = resolve_credentials(api_key: api_key, access_token: access_token)
|
|
234
|
+
http_client = build_http_client(**credentials, domain: resolve_domain(domain))
|
|
179
235
|
http_client.delete("/sandboxes/#{sandbox_id}")
|
|
180
236
|
true
|
|
181
237
|
rescue E2B::NotFoundError
|
|
182
238
|
true
|
|
183
239
|
end
|
|
184
240
|
|
|
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
241
|
end
|
|
198
242
|
|
|
199
243
|
# -------------------------------------------------------------------
|
|
@@ -232,7 +276,7 @@ module E2B
|
|
|
232
276
|
return false if @end_at && Time.now >= @end_at
|
|
233
277
|
|
|
234
278
|
get_info
|
|
235
|
-
|
|
279
|
+
@state != "paused"
|
|
236
280
|
rescue NotFoundError, E2BError
|
|
237
281
|
false
|
|
238
282
|
end
|
|
@@ -258,14 +302,22 @@ module E2B
|
|
|
258
302
|
# Pause the sandbox (saves state for later resume)
|
|
259
303
|
def pause
|
|
260
304
|
@http_client.post("/sandboxes/#{@sandbox_id}/pause")
|
|
305
|
+
@state = "paused"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Resume a paused sandbox
|
|
309
|
+
#
|
|
310
|
+
# @param timeout [Integer, nil] New timeout in seconds
|
|
311
|
+
def connect(timeout: nil)
|
|
312
|
+
resume(timeout: timeout)
|
|
313
|
+
self
|
|
261
314
|
end
|
|
262
315
|
|
|
263
316
|
# Resume a paused sandbox
|
|
264
317
|
#
|
|
265
318
|
# @param timeout [Integer, nil] New timeout in seconds
|
|
266
319
|
def resume(timeout: nil)
|
|
267
|
-
body = {}
|
|
268
|
-
body[:timeout] = timeout if timeout
|
|
320
|
+
body = { timeout: timeout || DEFAULT_TIMEOUT }
|
|
269
321
|
|
|
270
322
|
response = @http_client.post("/sandboxes/#{@sandbox_id}/connect", body: body)
|
|
271
323
|
process_sandbox_data(response) if response.is_a?(Hash)
|
|
@@ -275,7 +327,37 @@ module E2B
|
|
|
275
327
|
#
|
|
276
328
|
# @return [Hash] Snapshot info with snapshot_id
|
|
277
329
|
def create_snapshot
|
|
278
|
-
@http_client.post("/sandboxes/#{@sandbox_id}/snapshots")
|
|
330
|
+
response = @http_client.post("/sandboxes/#{@sandbox_id}/snapshots")
|
|
331
|
+
Models::SnapshotInfo.from_hash(response)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# List snapshots that were created from this sandbox.
|
|
335
|
+
#
|
|
336
|
+
# @param limit [Integer] Maximum results per page
|
|
337
|
+
# @param next_token [String, nil] Pagination token
|
|
338
|
+
# @return [SnapshotPaginator]
|
|
339
|
+
def list_snapshots(limit: 100, next_token: nil)
|
|
340
|
+
self.class.list_snapshots(
|
|
341
|
+
sandbox_id: @sandbox_id,
|
|
342
|
+
limit: limit,
|
|
343
|
+
next_token: next_token,
|
|
344
|
+
api_key: @api_key,
|
|
345
|
+
domain: @domain
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Get the MCP URL for the sandbox.
|
|
350
|
+
#
|
|
351
|
+
# @return [String]
|
|
352
|
+
def get_mcp_url
|
|
353
|
+
"https://#{get_host(MCP_PORT)}/mcp"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Get the MCP token for the sandbox.
|
|
357
|
+
#
|
|
358
|
+
# @return [String, nil]
|
|
359
|
+
def get_mcp_token
|
|
360
|
+
@mcp_token ||= @files.read("/etc/mcp-gateway/.token", user: "root")
|
|
279
361
|
end
|
|
280
362
|
|
|
281
363
|
# Get the host string for a port (without protocol)
|
|
@@ -299,12 +381,16 @@ module E2B
|
|
|
299
381
|
# @param path [String] File path in the sandbox
|
|
300
382
|
# @param user [String, nil] Username context
|
|
301
383
|
# @return [String] Download URL
|
|
302
|
-
def download_url(path, user: nil)
|
|
303
|
-
|
|
384
|
+
def download_url(path, user: nil, use_signature_expiration: nil)
|
|
385
|
+
user = resolve_legacy_file_user(user)
|
|
386
|
+
query = build_file_url_query(
|
|
387
|
+
path: path,
|
|
388
|
+
user: user,
|
|
389
|
+
operation: "read",
|
|
390
|
+
use_signature_expiration: use_signature_expiration
|
|
391
|
+
)
|
|
304
392
|
base = "https://#{Services::BaseService::ENVD_PORT}-#{@sandbox_id}.#{@domain}/files"
|
|
305
|
-
|
|
306
|
-
url += "&username=#{URI.encode_www_form_component(user)}" if user
|
|
307
|
-
url
|
|
393
|
+
query.empty? ? base : "#{base}?#{URI.encode_www_form(query)}"
|
|
308
394
|
end
|
|
309
395
|
|
|
310
396
|
# Get URL for uploading a file
|
|
@@ -312,12 +398,16 @@ module E2B
|
|
|
312
398
|
# @param path [String, nil] Destination path
|
|
313
399
|
# @param user [String, nil] Username context
|
|
314
400
|
# @return [String] Upload URL
|
|
315
|
-
def upload_url(path = nil, user: nil)
|
|
401
|
+
def upload_url(path = nil, user: nil, use_signature_expiration: nil)
|
|
402
|
+
user = resolve_legacy_file_user(user)
|
|
316
403
|
base = "https://#{Services::BaseService::ENVD_PORT}-#{@sandbox_id}.#{@domain}/files"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
404
|
+
query = build_file_url_query(
|
|
405
|
+
path: path,
|
|
406
|
+
user: user,
|
|
407
|
+
operation: "write",
|
|
408
|
+
use_signature_expiration: use_signature_expiration
|
|
409
|
+
)
|
|
410
|
+
query.empty? ? base : "#{base}?#{URI.encode_www_form(query)}"
|
|
321
411
|
end
|
|
322
412
|
|
|
323
413
|
# Get sandbox metrics (CPU, memory, disk usage)
|
|
@@ -371,8 +461,12 @@ module E2B
|
|
|
371
461
|
@cpu_count = data["cpuCount"] || data["cpu_count"] || data[:cpuCount]
|
|
372
462
|
@memory_mb = data["memoryMB"] || data["memory_mb"] || data[:memoryMB]
|
|
373
463
|
@metadata = data["metadata"] || data[:metadata] || {}
|
|
464
|
+
@state = data["state"] || data[:state] || @state
|
|
465
|
+
@domain = data["domain"] || data[:domain] || @domain
|
|
374
466
|
|
|
467
|
+
@envd_version = data["envdVersion"] || data["envd_version"] || data[:envdVersion] || @envd_version
|
|
375
468
|
@envd_access_token = data["envdAccessToken"] || data["envd_access_token"] || data[:envdAccessToken] || @envd_access_token
|
|
469
|
+
@traffic_access_token = data["trafficAccessToken"] || data["traffic_access_token"] || data[:trafficAccessToken] || @traffic_access_token
|
|
376
470
|
|
|
377
471
|
@started_at = parse_time(data["startedAt"] || data["started_at"] || data[:startedAt])
|
|
378
472
|
@end_at = parse_time(data["endAt"] || data["end_at"] || data[:endAt])
|
|
@@ -383,7 +477,8 @@ module E2B
|
|
|
383
477
|
sandbox_id: @sandbox_id,
|
|
384
478
|
sandbox_domain: @domain,
|
|
385
479
|
api_key: @api_key,
|
|
386
|
-
access_token: @envd_access_token
|
|
480
|
+
access_token: @envd_access_token,
|
|
481
|
+
envd_version: @envd_version
|
|
387
482
|
}
|
|
388
483
|
|
|
389
484
|
@commands = Services::Commands.new(**service_opts)
|
|
@@ -403,5 +498,61 @@ module E2B
|
|
|
403
498
|
rescue ArgumentError
|
|
404
499
|
nil
|
|
405
500
|
end
|
|
501
|
+
|
|
502
|
+
def resolve_legacy_file_user(user)
|
|
503
|
+
return user unless user.nil? || user.to_s.empty?
|
|
504
|
+
|
|
505
|
+
return Services::BaseService::DEFAULT_USERNAME if legacy_default_user?
|
|
506
|
+
|
|
507
|
+
nil
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def legacy_default_user?
|
|
511
|
+
return false if @envd_version.nil? || @envd_version.to_s.empty?
|
|
512
|
+
|
|
513
|
+
Gem::Version.new(@envd_version) < Services::BaseService::ENVD_DEFAULT_USER_VERSION
|
|
514
|
+
rescue ArgumentError
|
|
515
|
+
false
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def build_file_url_query(path:, user:, operation:, use_signature_expiration:)
|
|
519
|
+
if use_signature_expiration && !@envd_access_token
|
|
520
|
+
raise ArgumentError, "Signature expiration can be used only when the sandbox is secured"
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
query = []
|
|
524
|
+
query << ["path", path] if path
|
|
525
|
+
query << ["username", user] if user
|
|
526
|
+
|
|
527
|
+
signature = file_signature(
|
|
528
|
+
path: path || "",
|
|
529
|
+
operation: operation,
|
|
530
|
+
user: user,
|
|
531
|
+
expiration_in_seconds: use_signature_expiration
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return query unless signature
|
|
535
|
+
|
|
536
|
+
query << ["signature", signature[:signature]]
|
|
537
|
+
query << ["signature_expiration", signature[:expiration].to_s] if signature[:expiration]
|
|
538
|
+
query
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def file_signature(path:, operation:, user:, expiration_in_seconds:)
|
|
542
|
+
return nil unless @envd_access_token
|
|
543
|
+
|
|
544
|
+
expiration = expiration_in_seconds ? Time.now.to_i + expiration_in_seconds : nil
|
|
545
|
+
raw_user = user || ""
|
|
546
|
+
raw = if expiration
|
|
547
|
+
"#{path}:#{operation}:#{raw_user}:#{@envd_access_token}:#{expiration}"
|
|
548
|
+
else
|
|
549
|
+
"#{path}:#{operation}:#{raw_user}:#{@envd_access_token}"
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
digest = Digest::SHA256.digest(raw)
|
|
553
|
+
encoded = Base64.strict_encode64(digest).sub(/=+\z/, "")
|
|
554
|
+
|
|
555
|
+
{ signature: "v1_#{encoded}", expiration: expiration }
|
|
556
|
+
end
|
|
406
557
|
end
|
|
407
558
|
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module E2B
|
|
6
|
+
# Shared helpers used by both Sandbox class methods and Client.
|
|
7
|
+
#
|
|
8
|
+
# These methods are duplicated across both entry points to preserve
|
|
9
|
+
# the dual API surface (Sandbox.create vs Client#create). This module
|
|
10
|
+
# keeps them in a single place.
|
|
11
|
+
module SandboxHelpers
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def resolved_template(template, mcp:)
|
|
15
|
+
return template unless template.nil? || template.empty?
|
|
16
|
+
|
|
17
|
+
return Sandbox::DEFAULT_MCP_TEMPLATE if mcp
|
|
18
|
+
|
|
19
|
+
E2B.configuration&.default_template || "base"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def normalized_lifecycle(lifecycle:, auto_pause:)
|
|
23
|
+
raw_lifecycle = lifecycle || {
|
|
24
|
+
on_timeout: auto_pause ? "pause" : "kill",
|
|
25
|
+
auto_resume: false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
on_timeout = raw_lifecycle[:on_timeout] || raw_lifecycle["on_timeout"] || "kill"
|
|
29
|
+
unless %w[kill pause].include?(on_timeout)
|
|
30
|
+
raise ArgumentError, "Lifecycle on_timeout must be 'kill' or 'pause'"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
auto_resume = if raw_lifecycle.key?(:auto_resume)
|
|
34
|
+
raw_lifecycle[:auto_resume]
|
|
35
|
+
else
|
|
36
|
+
raw_lifecycle["auto_resume"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
on_timeout: on_timeout,
|
|
41
|
+
auto_resume: on_timeout == "pause" ? !!auto_resume : false
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def start_mcp_gateway(sandbox, mcp)
|
|
46
|
+
token = SecureRandom.uuid
|
|
47
|
+
sandbox.instance_variable_set(:@mcp_token, token)
|
|
48
|
+
sandbox.commands.run(
|
|
49
|
+
"mcp-gateway --config #{Shellwords.shellescape(JSON.generate(mcp))}",
|
|
50
|
+
user: "root",
|
|
51
|
+
envs: { "GATEWAY_ACCESS_TOKEN" => token }
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ensure_supported_envd_version!(response, http_client)
|
|
56
|
+
envd_version = response["envdVersion"] || response["envd_version"] || response[:envdVersion]
|
|
57
|
+
return if envd_version.nil?
|
|
58
|
+
return unless Gem::Version.new(envd_version) < Gem::Version.new("0.1.0")
|
|
59
|
+
|
|
60
|
+
sandbox_id = response["sandboxID"] || response["sandbox_id"] || response[:sandboxID]
|
|
61
|
+
begin
|
|
62
|
+
http_client.delete("/sandboxes/#{sandbox_id}") if sandbox_id
|
|
63
|
+
rescue NotFoundError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
raise TemplateError,
|
|
68
|
+
"You need to update the template to use the new SDK. You can do this by running `e2b template build` in the directory with the template."
|
|
69
|
+
rescue ArgumentError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def resolve_credentials(api_key:, access_token:)
|
|
74
|
+
resolved_api_key = api_key || E2B.configuration&.api_key || ENV["E2B_API_KEY"]
|
|
75
|
+
resolved_access_token = access_token || E2B.configuration&.access_token || ENV["E2B_ACCESS_TOKEN"]
|
|
76
|
+
|
|
77
|
+
unless (resolved_api_key && !resolved_api_key.empty?) || (resolved_access_token && !resolved_access_token.empty?)
|
|
78
|
+
raise ConfigurationError,
|
|
79
|
+
"E2B credentials are required. Set E2B_API_KEY or E2B_ACCESS_TOKEN, or pass api_key:/access_token:."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
{ api_key: resolved_api_key, access_token: resolved_access_token }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def resolve_domain(domain)
|
|
86
|
+
domain || E2B.configuration&.domain || ENV["E2B_DOMAIN"] || Configuration::DEFAULT_DOMAIN
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_http_client(api_key:, access_token:, domain:)
|
|
90
|
+
config = E2B.configuration
|
|
91
|
+
base_url = config&.api_url || ENV["E2B_API_URL"] || Configuration.default_api_url(domain)
|
|
92
|
+
API::HttpClient.new(
|
|
93
|
+
base_url: base_url,
|
|
94
|
+
api_key: api_key,
|
|
95
|
+
access_token: access_token,
|
|
96
|
+
logger: config&.logger
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|