e2b 0.3.0 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 103831a7280b1f523e4a7299820e40b328d0fd840f65850f42336f8c8b0e23ee
4
- data.tar.gz: d079861b11c20bc1e2280badc7e7e9032810a736a7bbca02a433802be27b2053
3
+ metadata.gz: ae958d79e7fb00debc2a5ea2690aeb1678e0d5ae5dad95d5db7b38a469da943c
4
+ data.tar.gz: d749554ef9cab965bd0711b010df41690c568e9992d22ae4bee0ac7377f8b95e
5
5
  SHA512:
6
- metadata.gz: 9fa1e68c98160a18f31afb994e66fc55d20647cc0769f80afbc8ab6fccc45df2c2e7d0f0033a37dd3e7a1571d373b0cd92060f98f6ab675241aa61309e0e70d9
7
- data.tar.gz: 759cffe0f051858f7f851f2f7599d495b24ca6abad1d313bed0cc96a7794e2a47eecacfdd6ecc81577d6fba18c9094824ec235022b8746047d630dce697ace0c
6
+ metadata.gz: ac6c3ec8f071cf7abadaf0750daa7900c7909edda3932b755523e82ad9d06e7dba7afb3f211b0f64ad928d52f3f7e7cacaf21244b32c52d3fb05c64683d7d4ef
7
+ data.tar.gz: a38f523b38adc6799fcca5bdeaf51efa4f29dc31ba0e3a56a2a9d817f727f974dc93ba417d6fa89129a5819f5377b6943b72d1a16c948b1ece1375dcc4aa035a
data/README.md CHANGED
@@ -132,6 +132,7 @@ sandbox = client.create(template: "base")
132
132
  | `list` | List running processes |
133
133
  | `kill(pid)` | Kill a process |
134
134
  | `send_stdin(pid, data)` | Send stdin to a process |
135
+ | `close_stdin(pid)` | Close stdin (send EOF) |
135
136
  | `connect(pid)` | Connect to running process |
136
137
 
137
138
  ### Filesystem (`sandbox.files`)
@@ -216,7 +216,7 @@ module E2B
216
216
  elsif value.is_a?(Time)
217
217
  value
218
218
  end
219
- rescue ArgumentError
219
+ rescue ArgumentError, TypeError
220
220
  nil
221
221
  end
222
222
 
@@ -102,8 +102,8 @@ module E2B
102
102
  return nil if value.nil?
103
103
  return value if value.is_a?(Time)
104
104
 
105
- Time.parse(value)
106
- rescue ArgumentError
105
+ Time.parse(value.to_s)
106
+ rescue ArgumentError, TypeError
107
107
  nil
108
108
  end
109
109
  end
@@ -29,8 +29,8 @@ module E2B
29
29
  return nil if value.nil?
30
30
  return value if value.is_a?(Time)
31
31
 
32
- Time.parse(value)
33
- rescue ArgumentError
32
+ Time.parse(value.to_s)
33
+ rescue ArgumentError, TypeError
34
34
  nil
35
35
  end
36
36
 
data/lib/e2b/sandbox.rb CHANGED
@@ -177,11 +177,15 @@ module E2B
177
177
 
178
178
  # List running sandboxes
179
179
  #
180
+ # Returns a paginator that yields sandbox info hashes one page at a time.
181
+ # Use {SandboxPaginator#has_next?} and {SandboxPaginator#next_items} to
182
+ # iterate through pages.
183
+ #
180
184
  # @param query [Hash, nil] Filter parameters (metadata, state)
181
185
  # @param limit [Integer] Maximum results per page
182
186
  # @param next_token [String, nil] Pagination token
183
187
  # @param api_key [String, nil] API key
184
- # @return [Array<Hash>] List of sandbox info hashes
188
+ # @return [SandboxPaginator]
185
189
  def list(query: nil, limit: 100, next_token: nil, api_key: nil, access_token: nil, domain: nil)
186
190
  credentials = resolve_credentials(api_key: api_key, access_token: access_token)
187
191
  http_client = build_http_client(**credentials, domain: resolve_domain(domain))
@@ -225,10 +229,15 @@ module E2B
225
229
  false
226
230
  end
227
231
 
228
- # Kill a sandbox by ID
232
+ # Kill a sandbox by ID (idempotent)
233
+ #
234
+ # Returns `true` if the sandbox was killed, *and* if it was already gone
235
+ # (NotFound). To distinguish "actually killed" from "already gone", use
236
+ # {.list} or {Sandbox#running?} before calling.
229
237
  #
230
238
  # @param sandbox_id [String] Sandbox ID to kill
231
239
  # @param api_key [String, nil] API key
240
+ # @return [Boolean] always true
232
241
  def kill(sandbox_id, api_key: nil, access_token: nil, domain: nil)
233
242
  credentials = resolve_credentials(api_key: api_key, access_token: access_token)
234
243
  http_client = build_http_client(**credentials, domain: resolve_domain(domain))
@@ -294,9 +303,16 @@ module E2B
294
303
  @end_at = Time.now + timeout
295
304
  end
296
305
 
297
- # Kill/terminate the sandbox
306
+ # Kill/terminate the sandbox (idempotent)
307
+ #
308
+ # Returns `true` whether the sandbox was running or already gone.
309
+ #
310
+ # @return [Boolean] always true
298
311
  def kill
299
312
  @http_client.delete("/sandboxes/#{@sandbox_id}")
313
+ true
314
+ rescue E2B::NotFoundError
315
+ true
300
316
  end
301
317
 
302
318
  # Pause the sandbox (saves state for later resume)
@@ -305,10 +321,16 @@ module E2B
305
321
  @state = "paused"
306
322
  end
307
323
 
308
- # Resume a paused sandbox
324
+ # @deprecated Use {#resume} instead. The instance-level `#connect`
325
+ # collides in name with the class-level {Sandbox.connect}, which has
326
+ # different semantics (it builds a new Sandbox instance from a
327
+ # sandbox_id). The instance method only resumes the current sandbox.
309
328
  #
310
329
  # @param timeout [Integer, nil] New timeout in seconds
311
330
  def connect(timeout: nil)
331
+ warn "[DEPRECATION] Sandbox#connect is deprecated; use Sandbox#resume " \
332
+ "instead. (Sandbox.connect class method is unchanged.) " \
333
+ "Called from #{caller(1, 1).first}"
312
334
  resume(timeout: timeout)
313
335
  self
314
336
  end
@@ -456,11 +478,13 @@ module E2B
456
478
 
457
479
  @sandbox_id = data["sandboxID"] || data["sandbox_id"] || data[:sandboxID] || @sandbox_id
458
480
  @template_id = data["templateID"] || data["template_id"] || data[:templateID] || @template_id
459
- @alias_name = data["alias"] || data[:alias]
460
- @client_id = data["clientID"] || data["client_id"] || data[:clientID]
461
- @cpu_count = data["cpuCount"] || data["cpu_count"] || data[:cpuCount]
462
- @memory_mb = data["memoryMB"] || data["memory_mb"] || data[:memoryMB]
463
- @metadata = data["metadata"] || data[:metadata] || {}
481
+ @alias_name = data["alias"] || data[:alias] || @alias_name
482
+ @client_id = data["clientID"] || data["client_id"] || data[:clientID] || @client_id
483
+ @cpu_count = data["cpuCount"] || data["cpu_count"] || data[:cpuCount] || @cpu_count
484
+ @memory_mb = data["memoryMB"] || data["memory_mb"] || data[:memoryMB] || @memory_mb
485
+ metadata = data["metadata"] || data[:metadata]
486
+ @metadata = metadata if metadata
487
+ @metadata ||= {}
464
488
  @state = data["state"] || data[:state] || @state
465
489
  @domain = data["domain"] || data[:domain] || @domain
466
490
 
@@ -468,8 +492,10 @@ module E2B
468
492
  @envd_access_token = data["envdAccessToken"] || data["envd_access_token"] || data[:envdAccessToken] || @envd_access_token
469
493
  @traffic_access_token = data["trafficAccessToken"] || data["traffic_access_token"] || data[:trafficAccessToken] || @traffic_access_token
470
494
 
471
- @started_at = parse_time(data["startedAt"] || data["started_at"] || data[:startedAt])
472
- @end_at = parse_time(data["endAt"] || data["end_at"] || data[:endAt])
495
+ started_at = parse_time(data["startedAt"] || data["started_at"] || data[:startedAt])
496
+ @started_at = started_at if started_at
497
+ end_at = parse_time(data["endAt"] || data["end_at"] || data[:endAt])
498
+ @end_at = end_at if end_at
473
499
  end
474
500
 
475
501
  def initialize_services
@@ -494,8 +520,8 @@ module E2B
494
520
  return nil if value.nil?
495
521
  return value if value.is_a?(Time)
496
522
 
497
- Time.parse(value)
498
- rescue ArgumentError
523
+ Time.parse(value.to_s)
524
+ rescue ArgumentError, TypeError
499
525
  nil
500
526
  end
501
527
 
@@ -3,7 +3,6 @@
3
3
  require "base64"
4
4
  require "net/http"
5
5
  require "openssl"
6
- require "ostruct"
7
6
  require "rubygems/version"
8
7
 
9
8
  module E2B
@@ -105,7 +104,13 @@ module E2B
105
104
  private
106
105
 
107
106
  def build_envd_client
108
- envd_url = "https://#{ENVD_PORT}-#{@sandbox_id}.#{@sandbox_domain}"
107
+ # Ensure envd traffic bypasses HTTP proxy — the proxy often can't
108
+ # CONNECT-tunnel to sandbox subdomains. Append the sandbox domain
109
+ # to no_proxy so Net::HTTP and Faraday connect directly.
110
+ ensure_no_proxy_for_domain!(@sandbox_domain)
111
+
112
+ scheme = ENV.fetch("E2B_ENVD_SCHEME", "https")
113
+ envd_url = "#{scheme}://#{ENVD_PORT}-#{@sandbox_id}.#{@sandbox_domain}"
109
114
 
110
115
  EnvdHttpClient.new(
111
116
  base_url: envd_url,
@@ -115,6 +120,19 @@ module E2B
115
120
  logger: @logger
116
121
  )
117
122
  end
123
+
124
+ # Append domain to no_proxy/NO_PROXY env vars at runtime so that
125
+ # both Net::HTTP and Faraday bypass the HTTP proxy for envd traffic.
126
+ def ensure_no_proxy_for_domain!(domain)
127
+ return if domain.nil? || domain.empty?
128
+
129
+ %w[no_proxy NO_PROXY].each do |var|
130
+ current = ENV[var].to_s
131
+ next if current.split(",").any? { |h| h.strip == domain }
132
+
133
+ ENV[var] = current.empty? ? domain : "#{current},#{domain}"
134
+ end
135
+ end
118
136
  end
119
137
 
120
138
  # HTTP client for envd daemon communication
@@ -125,6 +143,12 @@ module E2B
125
143
  class EnvdHttpClient
126
144
  DEFAULT_TIMEOUT = 120
127
145
 
146
+ RpcResponse = Struct.new(:status, :body, :headers, keyword_init: true) do
147
+ def success?
148
+ status >= 200 && status < 300
149
+ end
150
+ end
151
+
128
152
  def initialize(base_url:, api_key:, access_token: nil, sandbox_id:, logger: nil)
129
153
  @base_url = base_url.end_with?("/") ? base_url : "#{base_url}/"
130
154
  @api_key = api_key
@@ -182,6 +206,9 @@ module E2B
182
206
  return handle_streaming_rpc(path, envelope, timeout, on_event, headers)
183
207
  end
184
208
 
209
+ # Unary RPCs: try Connect protocol first, fall back to plain JSON.
210
+ # Some envd versions (e.g., 0.5.4 on self-hosted) reject
211
+ # application/connect+json for unary calls but accept application/json.
185
212
  handle_rpc_response(service, method) do
186
213
  with_retry("RPC #{service}/#{method}") do
187
214
  url = URI.parse("#{@base_url.chomp('/')}#{path}")
@@ -196,9 +223,21 @@ module E2B
196
223
 
197
224
  response = http.request(request)
198
225
 
199
- OpenStruct.new(
226
+ # Fall back to plain JSON if Connect protocol is unsupported (HTTP 415)
227
+ if response.code.to_i == 415
228
+ log_debug("Connect protocol unsupported for #{service}/#{method}, falling back to plain JSON")
229
+ request = Net::HTTP::Post.new(url.request_uri)
230
+ request["Content-Type"] = "application/json"
231
+ request["X-Access-Token"] = @access_token if @access_token
232
+ request["Connection"] = "keep-alive"
233
+ apply_custom_headers(request, headers)
234
+ request.body = json_body
235
+
236
+ response = http.request(request)
237
+ end
238
+
239
+ RpcResponse.new(
200
240
  status: response.code.to_i,
201
- success?: response.code.to_i >= 200 && response.code.to_i < 300,
202
241
  body: response.body,
203
242
  headers: response.to_hash
204
243
  )
@@ -206,98 +245,116 @@ module E2B
206
245
  end
207
246
  end
208
247
 
209
- # Streaming RPC with chunked response processing
248
+ # Streaming RPC with chunked response processing.
249
+ # Uses Faraday for the HTTP connection (same as non-streaming RPCs) to
250
+ # inherit proxy configuration and SSL settings. The streaming is handled
251
+ # via Faraday's on_data callback for chunked response processing.
252
+ #
253
+ # Streaming RPCs are NOT idempotent (e.g. process.Process/Start spawns a
254
+ # process), so we only retry while no events have been emitted to the
255
+ # caller yet. Once any byte has been delivered via on_event, a retry
256
+ # would replay output AND start a second process server-side.
210
257
  def handle_streaming_rpc(path, envelope, timeout, on_event, headers)
211
258
  result = { events: [], stdout: "", stderr: "", exit_code: nil }
212
259
  buffer = "".b
213
260
 
214
- url = URI.parse("#{@base_url.chomp('/')}#{path}")
215
-
216
- with_retry("Streaming RPC #{path}") do
217
- http = build_http(url, timeout)
261
+ full_path = normalize_path(path)
218
262
 
219
- request = Net::HTTP::Post.new(url.request_uri)
220
- request["Content-Type"] = "application/connect+json"
221
- request["X-Access-Token"] = @access_token if @access_token
222
- request["Connection"] = "keep-alive"
223
- apply_custom_headers(request, headers)
224
- request.body = envelope
263
+ with_retry("Streaming RPC #{path}", abort_if: -> { result[:events].any? }) do
264
+ ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
225
265
 
226
- http.start do |conn|
227
- conn.request(request) do |response|
228
- unless response.code.to_i.between?(200, 299)
229
- body = response.body
230
- handle_error(OpenStruct.new(status: response.code.to_i, success?: false, body: body, headers: response.to_hash))
231
- end
266
+ streaming_conn = Faraday.new(url: @base_url, ssl: { verify: ssl_verify }) do |conn|
267
+ conn.options.timeout = timeout
268
+ conn.options.open_timeout = 30
269
+ conn.adapter Faraday.default_adapter
270
+ end
232
271
 
233
- response.read_body do |chunk|
234
- next if chunk.nil? || chunk.empty?
235
- buffer << chunk
272
+ req_headers = {
273
+ "Content-Type" => "application/connect+json",
274
+ "Connection" => "keep-alive",
275
+ "E2b-Sandbox-Id" => @sandbox_id,
276
+ "E2b-Sandbox-Port" => "#{BaseService::ENVD_PORT}",
277
+ "X-API-Key" => @api_key
278
+ }
279
+ req_headers["X-Access-Token"] = @access_token if @access_token
280
+ if headers
281
+ headers.each { |k, v| req_headers[k.to_s] = v.to_s if v }
282
+ end
236
283
 
237
- while buffer.bytesize >= 5
238
- flags = buffer.getbyte(0)
239
- length = buffer.byteslice(1, 4).unpack1("N")
284
+ response = streaming_conn.post(full_path) do |req|
285
+ req.headers.merge!(req_headers)
286
+ req.body = envelope
287
+ req.options.on_data = proc do |chunk, _overall_size, _env|
288
+ next if chunk.nil? || chunk.empty?
289
+ buffer << chunk
240
290
 
241
- break if length.nil? || buffer.bytesize < 5 + length
291
+ while buffer.bytesize >= 5
292
+ flags = buffer.getbyte(0)
293
+ length = buffer.byteslice(1, 4).unpack1("N")
242
294
 
243
- message_bytes = buffer.byteslice(5, length)
244
- buffer = buffer.byteslice(5 + length..-1) || "".b
295
+ break if length.nil? || buffer.bytesize < 5 + length
245
296
 
246
- next if message_bytes.nil? || message_bytes.empty?
297
+ message_bytes = buffer.byteslice(5, length)
298
+ buffer = buffer.byteslice(5 + length..-1) || "".b
247
299
 
248
- message_str = message_bytes.force_encoding("UTF-8")
300
+ next if message_bytes.nil? || message_bytes.empty?
249
301
 
250
- begin
251
- msg = JSON.parse(message_str)
252
- msg = msg["result"] if msg["result"]
302
+ message_str = message_bytes.force_encoding("UTF-8")
253
303
 
254
- result[:events] << msg
304
+ begin
305
+ msg = JSON.parse(message_str)
306
+ msg = msg["result"] if msg["result"]
255
307
 
256
- stdout_data = nil
257
- stderr_data = nil
308
+ result[:events] << msg
258
309
 
259
- if msg["event"]
260
- event = msg["event"]
310
+ stdout_data = nil
311
+ stderr_data = nil
261
312
 
262
- data_event = event["Data"] || event["data"]
263
- if data_event
264
- stdout_data = decode_base64(data_event["stdout"]) if data_event["stdout"]
265
- stderr_data = decode_base64(data_event["stderr"]) if data_event["stderr"]
266
- result[:stdout] += stdout_data if stdout_data
267
- result[:stderr] += stderr_data if stderr_data
268
- end
313
+ if msg["event"]
314
+ event = msg["event"]
269
315
 
270
- end_event = event["End"] || event["end"]
271
- if end_event
272
- result[:exit_code] = parse_exit_code(end_event["exitCode"] || end_event["exit_code"] || end_event["status"])
273
- end
316
+ data_event = event["Data"] || event["data"]
317
+ if data_event
318
+ stdout_data = decode_base64(data_event["stdout"]) if data_event["stdout"]
319
+ stderr_data = decode_base64(data_event["stderr"]) if data_event["stderr"]
320
+ result[:stdout] += stdout_data if stdout_data
321
+ result[:stderr] += stderr_data if stderr_data
274
322
  end
275
323
 
276
- if msg["stdout"]
277
- stdout_data = decode_base64(msg["stdout"])
278
- result[:stdout] += stdout_data
279
- end
280
- if msg["stderr"]
281
- stderr_data = decode_base64(msg["stderr"])
282
- result[:stderr] += stderr_data
283
- end
284
- if msg["exitCode"] || msg["exit_code"]
285
- result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"])
324
+ end_event = event["End"] || event["end"]
325
+ if end_event
326
+ result[:exit_code] = parse_exit_code(end_event["exitCode"] || end_event["exit_code"] || end_event["status"])
286
327
  end
328
+ end
287
329
 
288
- on_event.call(
289
- stdout: stdout_data,
290
- stderr: stderr_data,
291
- exit_code: result[:exit_code],
292
- event: msg
293
- )
294
- rescue JSON::ParserError
295
- # Skip unparseable messages
330
+ if msg["stdout"]
331
+ stdout_data = decode_base64(msg["stdout"])
332
+ result[:stdout] += stdout_data
333
+ end
334
+ if msg["stderr"]
335
+ stderr_data = decode_base64(msg["stderr"])
336
+ result[:stderr] += stderr_data
296
337
  end
338
+ if msg["exitCode"] || msg["exit_code"]
339
+ result[:exit_code] = parse_exit_code(msg["exitCode"] || msg["exit_code"])
340
+ end
341
+
342
+ on_event.call(
343
+ stdout: stdout_data,
344
+ stderr: stderr_data,
345
+ exit_code: result[:exit_code],
346
+ event: msg
347
+ )
348
+ rescue JSON::ParserError
349
+ # Skip unparseable messages
297
350
  end
298
351
  end
299
352
  end
300
353
  end
354
+
355
+ unless response.status.between?(200, 299)
356
+ handle_error(response)
357
+ end
301
358
  end
302
359
 
303
360
  result
@@ -328,24 +385,55 @@ module E2B
328
385
  end
329
386
 
330
387
  def build_http(url, timeout)
331
- http = Net::HTTP.new(url.host, url.port)
332
- http.use_ssl = true
388
+ # Respect HTTP proxy env vars (http_proxy, https_proxy, no_proxy)
389
+ # Faraday handles this automatically for non-streaming RPCs, but
390
+ # Net::HTTP requires explicit proxy configuration.
391
+ proxy = resolve_proxy(url)
392
+ http = if proxy
393
+ Net::HTTP.new(url.host, url.port, proxy.host, proxy.port, proxy.user, proxy.password)
394
+ else
395
+ Net::HTTP.new(url.host, url.port)
396
+ end
397
+
398
+ http.use_ssl = (url.scheme == "https")
333
399
  http.open_timeout = 30
334
400
  http.read_timeout = timeout
335
401
  http.keep_alive_timeout = 30
336
402
 
337
- ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
338
- http.verify_mode = ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
403
+ if http.use_ssl?
404
+ ssl_verify = ENV.fetch("E2B_SSL_VERIFY", "true").downcase != "false"
405
+ http.verify_mode = ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
406
+ end
339
407
 
340
408
  http
341
409
  end
342
410
 
343
- def with_retry(operation, max_retries: 3)
411
+ def resolve_proxy(url)
412
+ no_proxy = ENV["no_proxy"] || ENV["NO_PROXY"]
413
+ if no_proxy
414
+ no_proxy_hosts = no_proxy.split(",").map(&:strip)
415
+ return nil if no_proxy_hosts.any? { |h| url.host.end_with?(h) || h == "*" }
416
+ end
417
+
418
+ proxy_env = url.scheme == "https" ? (ENV["https_proxy"] || ENV["HTTPS_PROXY"]) : (ENV["http_proxy"] || ENV["HTTP_PROXY"])
419
+ return nil unless proxy_env
420
+
421
+ URI.parse(proxy_env)
422
+ rescue URI::InvalidURIError
423
+ nil
424
+ end
425
+
426
+ def with_retry(operation, max_retries: 3, abort_if: nil)
344
427
  retry_count = 0
345
428
 
346
429
  begin
347
430
  yield
348
431
  rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET, EOFError, Net::OpenTimeout, Net::ReadTimeout => e
432
+ if abort_if && abort_if.call
433
+ log_debug("#{operation}: not retrying (#{e.class}); request had observable side effects")
434
+ raise E2B::E2BError, "#{operation} failed after partial response: #{e.message}"
435
+ end
436
+
349
437
  retry_count += 1
350
438
 
351
439
  if retry_count <= max_retries
@@ -467,7 +555,11 @@ module E2B
467
555
  def create_connect_envelope(json_message)
468
556
  flags = "\x00".b
469
557
  length = [json_message.bytesize].pack("N")
470
- flags + length + json_message
558
+ # Force binary encoding on the payload so concatenation with the binary
559
+ # frame header (flags + length) doesn't raise Encoding::CompatibilityError
560
+ # when the packed length contains bytes >= 0x80 (json_message.bytesize >= 32768)
561
+ # or json_message itself has multibyte UTF-8 characters.
562
+ flags + length + json_message.b
471
563
  end
472
564
 
473
565
  def parse_exit_code(value)
@@ -480,7 +572,7 @@ module E2B
480
572
  elsif str =~ /^(\d+)$/
481
573
  $1.to_i
482
574
  else
483
- str.include?("0") ? 0 : 1
575
+ 1
484
576
  end
485
577
  end
486
578
 
@@ -166,6 +166,22 @@ module E2B
166
166
  timeout: request_timeout || 30)
167
167
  end
168
168
 
169
+ # Close the stdin of a running process.
170
+ #
171
+ # After calling this, no more input can be sent to the process via
172
+ # {#send_stdin}.
173
+ #
174
+ # @param pid [Integer] Process ID
175
+ # @param request_timeout [Integer, nil] Request timeout in seconds
176
+ # @return [void]
177
+ # @raise [E2B::E2BError] if the process is not found
178
+ def close_stdin(pid, request_timeout: nil, headers: nil)
179
+ envd_rpc("process.Process", "CloseStdin",
180
+ body: { process: { pid: pid } },
181
+ headers: headers,
182
+ timeout: request_timeout || 30)
183
+ end
184
+
169
185
  # Connect to a running process
170
186
  #
171
187
  # @param pid [Integer] Process ID to connect to
@@ -109,6 +109,10 @@ module E2B
109
109
 
110
110
  # Check if a path exists
111
111
  #
112
+ # Only NotFoundError is treated as "does not exist". Other errors
113
+ # (auth, network, server) propagate so callers can distinguish
114
+ # "file is gone" from "we couldn't ask".
115
+ #
112
116
  # @param path [String] Path to check
113
117
  # @param user [String] Username context
114
118
  # @param request_timeout [Integer] Request timeout in seconds
@@ -116,7 +120,7 @@ module E2B
116
120
  def exists?(path, user: nil, request_timeout: 30)
117
121
  get_info(path, user: user, request_timeout: request_timeout)
118
122
  true
119
- rescue E2B::NotFoundError, E2B::E2BError
123
+ rescue E2B::NotFoundError
120
124
  false
121
125
  end
122
126
 
data/lib/e2b/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module E2B
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.2"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: e2b
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tao Luo
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-13 00:00:00.000000000 Z
10
+ date: 2026-04-30 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: faraday
@@ -57,20 +57,6 @@ dependencies:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
59
  version: '0.2'
60
- - !ruby/object:Gem::Dependency
61
- name: ostruct
62
- requirement: !ruby/object:Gem::Requirement
63
- requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '0.6'
67
- type: :runtime
68
- prerelease: false
69
- version_requirements: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - "~>"
72
- - !ruby/object:Gem::Version
73
- version: '0.6'
74
60
  - !ruby/object:Gem::Dependency
75
61
  name: rake
76
62
  requirement: !ruby/object:Gem::Requirement