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.
@@ -4,6 +4,7 @@ require "base64"
4
4
  require "net/http"
5
5
  require "openssl"
6
6
  require "ostruct"
7
+ require "rubygems/version"
7
8
 
8
9
  module E2B
9
10
  module Services
@@ -15,17 +16,21 @@ module E2B
15
16
  class BaseService
16
17
  # Default envd port
17
18
  ENVD_PORT = 49983
19
+ DEFAULT_USERNAME = "user"
20
+ ENVD_DEFAULT_USER_VERSION = Gem::Version.new("0.4.0")
21
+ ENVD_RECURSIVE_WATCH_VERSION = Gem::Version.new("0.1.4")
18
22
 
19
23
  # @param sandbox_id [String] Sandbox ID
20
24
  # @param sandbox_domain [String] Sandbox domain (e.g., "e2b.app")
21
25
  # @param api_key [String] API key for authentication
22
26
  # @param access_token [String, nil] Sandbox-specific access token
23
27
  # @param logger [Logger, nil] Optional logger
24
- def initialize(sandbox_id:, sandbox_domain:, api_key:, access_token: nil, logger: nil)
28
+ def initialize(sandbox_id:, sandbox_domain:, api_key:, access_token: nil, envd_version: nil, logger: nil)
25
29
  @sandbox_id = sandbox_id
26
30
  @sandbox_domain = sandbox_domain
27
31
  @api_key = api_key
28
32
  @access_token = access_token
33
+ @envd_version = envd_version
29
34
  @logger = logger
30
35
  @envd_client = nil
31
36
  end
@@ -40,18 +45,18 @@ module E2B
40
45
  end
41
46
 
42
47
  # Perform GET request to envd
43
- def envd_get(path, params: {}, timeout: 120)
44
- envd_client.get(path, params: params, timeout: timeout)
48
+ def envd_get(path, params: {}, timeout: 120, headers: nil)
49
+ envd_client.get(path, params: params, timeout: timeout, headers: headers)
45
50
  end
46
51
 
47
52
  # Perform POST request to envd
48
- def envd_post(path, body: nil, timeout: 120)
49
- envd_client.post(path, body: body, timeout: timeout)
53
+ def envd_post(path, body: nil, timeout: 120, headers: nil)
54
+ envd_client.post(path, body: body, timeout: timeout, headers: headers)
50
55
  end
51
56
 
52
57
  # Perform DELETE request to envd
53
- def envd_delete(path, timeout: 120)
54
- envd_client.delete(path, timeout: timeout)
58
+ def envd_delete(path, timeout: 120, headers: nil)
59
+ envd_client.delete(path, timeout: timeout, headers: headers)
55
60
  end
56
61
 
57
62
  # Perform Connect RPC call to envd
@@ -62,8 +67,39 @@ module E2B
62
67
  # @param timeout [Integer] Request timeout in seconds
63
68
  # @param on_event [Proc, nil] Callback for streaming events
64
69
  # @return [Hash] Response with :events, :stdout, :stderr, :exit_code
65
- def envd_rpc(service, method, body: {}, timeout: 120, on_event: nil)
66
- envd_client.rpc(service, method, body: body, timeout: timeout, on_event: on_event)
70
+ def envd_rpc(service, method, body: {}, timeout: 120, on_event: nil, headers: nil)
71
+ envd_client.rpc(service, method, body: body, timeout: timeout, on_event: on_event, headers: headers)
72
+ end
73
+
74
+ def user_auth_headers(user)
75
+ resolved_user = resolve_username(user)
76
+ return nil if resolved_user.nil? || resolved_user.to_s.empty?
77
+
78
+ encoded = Base64.strict_encode64("#{resolved_user}:")
79
+ { "Authorization" => "Basic #{encoded}" }
80
+ end
81
+
82
+ def resolve_username(user)
83
+ return user unless user.nil? || user.to_s.empty?
84
+ return DEFAULT_USERNAME if legacy_default_user?
85
+
86
+ nil
87
+ end
88
+
89
+ def legacy_default_user?
90
+ return false if @envd_version.nil? || @envd_version.to_s.empty?
91
+
92
+ Gem::Version.new(@envd_version) < ENVD_DEFAULT_USER_VERSION
93
+ rescue ArgumentError
94
+ false
95
+ end
96
+
97
+ def supports_recursive_watch?
98
+ return true if @envd_version.nil? || @envd_version.to_s.empty?
99
+
100
+ Gem::Version.new(@envd_version) >= ENVD_RECURSIVE_WATCH_VERSION
101
+ rescue ArgumentError
102
+ true
67
103
  end
68
104
 
69
105
  private
@@ -98,28 +134,31 @@ module E2B
98
134
  @connection = build_connection
99
135
  end
100
136
 
101
- def get(path, params: {}, timeout: DEFAULT_TIMEOUT)
137
+ def get(path, params: {}, timeout: DEFAULT_TIMEOUT, headers: nil)
102
138
  handle_response do
103
139
  @connection.get(normalize_path(path)) do |req|
104
140
  req.params = params
105
141
  req.options.timeout = timeout
142
+ req.headers.update(headers) if headers
106
143
  end
107
144
  end
108
145
  end
109
146
 
110
- def post(path, body: nil, timeout: DEFAULT_TIMEOUT)
147
+ def post(path, body: nil, timeout: DEFAULT_TIMEOUT, headers: nil)
111
148
  handle_response do
112
149
  @connection.post(normalize_path(path)) do |req|
113
150
  req.body = body.to_json if body
114
151
  req.options.timeout = timeout
152
+ req.headers.update(headers) if headers
115
153
  end
116
154
  end
117
155
  end
118
156
 
119
- def delete(path, timeout: DEFAULT_TIMEOUT)
157
+ def delete(path, timeout: DEFAULT_TIMEOUT, headers: nil)
120
158
  handle_response do
121
159
  @connection.delete(normalize_path(path)) do |req|
122
160
  req.options.timeout = timeout
161
+ req.headers.update(headers) if headers
123
162
  end
124
163
  end
125
164
  end
@@ -132,7 +171,7 @@ module E2B
132
171
  # @param timeout [Integer] Timeout in seconds
133
172
  # @param on_event [Proc, nil] Callback for streaming events
134
173
  # @return [Hash] Response
135
- def rpc(service, method, body: {}, timeout: DEFAULT_TIMEOUT, on_event: nil)
174
+ def rpc(service, method, body: {}, timeout: DEFAULT_TIMEOUT, on_event: nil, headers: nil)
136
175
  path = "/#{service}/#{method}"
137
176
  json_body = body.to_json
138
177
  envelope = create_connect_envelope(json_body)
@@ -140,7 +179,7 @@ module E2B
140
179
  log_debug("RPC #{service}/#{method}")
141
180
 
142
181
  if on_event
143
- return handle_streaming_rpc(path, envelope, timeout, on_event)
182
+ return handle_streaming_rpc(path, envelope, timeout, on_event, headers)
144
183
  end
145
184
 
146
185
  handle_rpc_response(service, method) do
@@ -152,6 +191,7 @@ module E2B
152
191
  request["Content-Type"] = "application/connect+json"
153
192
  request["X-Access-Token"] = @access_token if @access_token
154
193
  request["Connection"] = "keep-alive"
194
+ apply_custom_headers(request, headers)
155
195
  request.body = envelope
156
196
 
157
197
  response = http.request(request)
@@ -167,7 +207,7 @@ module E2B
167
207
  end
168
208
 
169
209
  # Streaming RPC with chunked response processing
170
- def handle_streaming_rpc(path, envelope, timeout, on_event)
210
+ def handle_streaming_rpc(path, envelope, timeout, on_event, headers)
171
211
  result = { events: [], stdout: "", stderr: "", exit_code: nil }
172
212
  buffer = "".b
173
213
 
@@ -180,6 +220,7 @@ module E2B
180
220
  request["Content-Type"] = "application/connect+json"
181
221
  request["X-Access-Token"] = @access_token if @access_token
182
222
  request["Connection"] = "keep-alive"
223
+ apply_custom_headers(request, headers)
183
224
  request.body = envelope
184
225
 
185
226
  http.start do |conn|
@@ -480,6 +521,14 @@ module E2B
480
521
  def log_debug(message)
481
522
  @logger&.debug("[E2B] #{message}")
482
523
  end
524
+
525
+ def apply_custom_headers(request, headers)
526
+ return unless headers
527
+
528
+ headers.each do |key, value|
529
+ request[key] = value
530
+ end
531
+ end
483
532
  end
484
533
  end
485
534
  end
@@ -4,6 +4,65 @@ require "base64"
4
4
 
5
5
  module E2B
6
6
  module Services
7
+ # Thread-safe buffer for live command events.
8
+ #
9
+ # Allows a producer thread to push envd events while the command handle
10
+ # drains them on demand from {CommandHandle#each} or {CommandHandle#wait}.
11
+ class LiveEventStream
12
+ def initialize
13
+ @mutex = Mutex.new
14
+ @condition = ConditionVariable.new
15
+ @items = []
16
+ @closed = false
17
+ end
18
+
19
+ def push(event)
20
+ @mutex.synchronize do
21
+ return if @closed
22
+
23
+ @items << [:event, event]
24
+ @condition.signal
25
+ end
26
+ end
27
+
28
+ def fail(error)
29
+ @mutex.synchronize do
30
+ return if @closed
31
+
32
+ @items << [:error, error]
33
+ @condition.signal
34
+ end
35
+ end
36
+
37
+ def close(discard_pending: false)
38
+ @mutex.synchronize do
39
+ return if @closed
40
+
41
+ @closed = true
42
+ @items.clear if discard_pending
43
+ @condition.broadcast
44
+ end
45
+ end
46
+
47
+ def each
48
+ return enum_for(:each) unless block_given?
49
+
50
+ loop do
51
+ item = @mutex.synchronize do
52
+ @condition.wait(@mutex) while @items.empty? && !@closed
53
+ @items.shift
54
+ end
55
+
56
+ break unless item
57
+
58
+ type, payload = item
59
+ raise payload if type == :error
60
+
61
+ yield payload
62
+ end
63
+ end
64
+ end
65
+
7
66
  # Result of a command execution in the sandbox.
8
67
  #
9
68
  # Returned by {CommandHandle#wait} when the command finishes successfully.
@@ -92,25 +151,29 @@ module E2B
92
151
  # @param pid [Integer, nil] Process ID
93
152
  # @param handle_kill [Proc] Proc that sends SIGKILL to the process
94
153
  # @param handle_send_stdin [Proc] Proc that sends data to stdin/pty
154
+ # @param handle_disconnect [Proc, nil] Proc that disconnects the event stream
95
155
  # @param events_proc [Proc, nil] Proc that accepts a block and yields
96
156
  # parsed events as they arrive. Each event is a Hash with keys like
97
157
  # "event" => { "Start" => ..., "Data" => ..., "End" => ... }.
98
158
  # May be nil if the result is already materialized.
99
159
  # @param result [Hash, nil] Pre-materialized result from a synchronous
100
160
  # RPC call. Expected keys: :events, :stdout, :stderr, :exit_code.
101
- def initialize(pid:, handle_kill:, handle_send_stdin:, events_proc: nil, result: nil)
161
+ def initialize(pid:, handle_kill:, handle_send_stdin:, handle_disconnect: nil, events_proc: nil, result: nil)
102
162
  @pid = pid
103
163
  @handle_kill = handle_kill
104
164
  @handle_send_stdin = handle_send_stdin
165
+ @handle_disconnect = handle_disconnect
105
166
  @events_proc = events_proc
106
167
  @result = result
107
168
  @disconnected = false
108
169
  @finished = false
170
+ @completed = false
109
171
  @stdout = ""
110
172
  @stderr = ""
111
173
  @exit_code = nil
112
174
  @error = nil
113
175
  @mutex = Mutex.new
176
+ @materialized_event_index = 0
114
177
  end
115
178
 
116
179
  # Wait for the command to finish and return the result.
@@ -126,6 +189,10 @@ module E2B
126
189
  # @raise [CommandExitError] if exit code is non-zero
127
190
  def wait(on_stdout: nil, on_stderr: nil, on_pty: nil)
128
191
  consume_events(on_stdout: on_stdout, on_stderr: on_stderr, on_pty: on_pty)
192
+ unless @disconnected || completed?
193
+ raise E2BError, "Command ended without an end event"
194
+ end
195
+
129
196
  build_result.tap do |cmd_result|
130
197
  unless cmd_result.success?
131
198
  raise CommandExitError.new(
@@ -162,7 +229,10 @@ module E2B
162
229
  #
163
230
  # @return [void]
164
231
  def disconnect
232
+ return if @disconnected
233
+
165
234
  @disconnected = true
235
+ @handle_disconnect&.call
166
236
  end
167
237
 
168
238
  # Iterate over command output as it arrives.
@@ -201,11 +271,9 @@ module E2B
201
271
  each do |stdout_chunk, stderr_chunk, pty_chunk|
202
272
  if stdout_chunk
203
273
  on_stdout&.call(stdout_chunk)
204
- @mutex.synchronize { @stdout += stdout_chunk }
205
274
  end
206
275
  if stderr_chunk
207
276
  on_stderr&.call(stderr_chunk)
208
- @mutex.synchronize { @stderr += stderr_chunk }
209
277
  end
210
278
  if pty_chunk
211
279
  on_pty&.call(pty_chunk)
@@ -222,22 +290,12 @@ module E2B
222
290
  #
223
291
  # @return [CommandResult]
224
292
  def build_result
225
- if @result && !@finished
226
- # Use the pre-materialized result from the synchronous RPC call
227
- CommandResult.new(
228
- stdout: @result[:stdout] || "",
229
- stderr: @result[:stderr] || "",
230
- exit_code: @result[:exit_code] || 0,
231
- error: @result[:error]
232
- )
233
- else
234
- CommandResult.new(
235
- stdout: @stdout,
236
- stderr: @stderr,
237
- exit_code: @exit_code || 0,
238
- error: @error
239
- )
240
- end
293
+ CommandResult.new(
294
+ stdout: resolved_stdout,
295
+ stderr: resolved_stderr,
296
+ exit_code: resolved_exit_code || 0,
297
+ error: resolved_error
298
+ )
241
299
  end
242
300
 
243
301
  # Iterate over events from a pre-materialized result hash.
@@ -249,14 +307,14 @@ module E2B
249
307
  # @yield [stdout, stderr, pty]
250
308
  # @return [void]
251
309
  def iterate_materialized_events
252
- events = @result[:events] || []
253
- events.each do |event_hash|
310
+ events = result_value(:events) || []
311
+ while @materialized_event_index < events.length
254
312
  break if @disconnected
255
313
 
256
- next unless event_hash.is_a?(Hash) && event_hash["event"]
314
+ event_hash = events[@materialized_event_index]
315
+ @materialized_event_index += 1
257
316
 
258
- event = event_hash["event"]
259
- process_event(event) do |stdout_chunk, stderr_chunk, pty_chunk|
317
+ process_message(event_hash) do |stdout_chunk, stderr_chunk, pty_chunk|
260
318
  yield stdout_chunk, stderr_chunk, pty_chunk
261
319
  end
262
320
  end
@@ -270,25 +328,58 @@ module E2B
270
328
  # @yield [stdout, stderr, pty]
271
329
  # @return [void]
272
330
  def iterate_streaming_events
273
- @events_proc.call do |event_hash|
274
- break if @disconnected
275
-
276
- next unless event_hash.is_a?(Hash) && event_hash["event"]
331
+ catch(:stop_iteration) do
332
+ @events_proc.call do |event_hash|
333
+ throw :stop_iteration if @disconnected
277
334
 
278
- event = event_hash["event"]
279
- process_event(event) do |stdout_chunk, stderr_chunk, pty_chunk|
280
- yield stdout_chunk, stderr_chunk, pty_chunk
335
+ process_message(event_hash) do |stdout_chunk, stderr_chunk, pty_chunk|
336
+ yield stdout_chunk, stderr_chunk, pty_chunk
337
+ end
281
338
  end
282
339
  end
283
340
  end
284
341
 
285
- # Process a single event from the stream, extracting output data.
342
+ # Process a single stream message, extracting output data.
343
+ #
344
+ # Messages usually arrive with an "event" wrapper from the envd process
345
+ # service, but some paths also surface top-level stdout/stderr/exitCode
346
+ # fields. This method handles both shapes.
286
347
  #
287
348
  # Event shapes from the envd process service:
288
349
  # - Start: { "Start" => { "pid" => 123 } }
289
350
  # - Data: { "Data" => { "stdout" => "base64", "stderr" => "base64", "pty" => "base64" } }
290
351
  # - End: { "End" => { "exitCode" => 0, "error" => "...", "status" => "..." } }
291
352
  #
353
+ # @param message [Hash] A raw stream message
354
+ # @yield [stdout, stderr, pty]
355
+ # @return [void]
356
+ def process_message(message)
357
+ return unless message.is_a?(Hash)
358
+
359
+ event = message["event"]
360
+ process_event(event) { |stdout_chunk, stderr_chunk, pty_chunk| yield stdout_chunk, stderr_chunk, pty_chunk } if event.is_a?(Hash)
361
+
362
+ if event.nil?
363
+ stdout_chunk = decode_base64(message["stdout"])
364
+ stderr_chunk = decode_base64(message["stderr"])
365
+
366
+ if stdout_chunk && !stdout_chunk.empty?
367
+ append_stdout(stdout_chunk)
368
+ yield(stdout_chunk, nil, nil)
369
+ end
370
+
371
+ if stderr_chunk && !stderr_chunk.empty?
372
+ append_stderr(stderr_chunk)
373
+ yield(nil, stderr_chunk, nil)
374
+ end
375
+ end
376
+
377
+ exit_value = message["exitCode"] || message["exit_code"]
378
+ complete!(exit_value, message["error"]) if exit_value
379
+ end
380
+
381
+ # Process a single event from the stream, extracting output data.
382
+ #
292
383
  # @param event [Hash] The event sub-hash (value of "event" key)
293
384
  # @yield [stdout, stderr, pty]
294
385
  # @return [void]
@@ -300,8 +391,16 @@ module E2B
300
391
  stderr_chunk = decode_base64(data_event["stderr"])
301
392
  pty_chunk = decode_base64(data_event["pty"])
302
393
 
303
- yield(stdout_chunk, nil, nil) if stdout_chunk && !stdout_chunk.empty?
304
- yield(nil, stderr_chunk, nil) if stderr_chunk && !stderr_chunk.empty?
394
+ if stdout_chunk && !stdout_chunk.empty?
395
+ append_stdout(stdout_chunk)
396
+ yield(stdout_chunk, nil, nil)
397
+ end
398
+
399
+ if stderr_chunk && !stderr_chunk.empty?
400
+ append_stderr(stderr_chunk)
401
+ yield(nil, stderr_chunk, nil)
402
+ end
403
+
305
404
  yield(nil, nil, pty_chunk) if pty_chunk && !pty_chunk.empty?
306
405
  end
307
406
 
@@ -310,8 +409,62 @@ module E2B
310
409
  return unless end_event
311
410
 
312
411
  exit_value = end_event["exitCode"] || end_event["exit_code"] || end_event["status"]
313
- @exit_code = parse_exit_code(exit_value)
314
- @error = end_event["error"] if end_event["error"] && !end_event["error"].empty?
412
+ complete!(exit_value, end_event["error"])
413
+ end
414
+
415
+ def append_stdout(chunk)
416
+ @mutex.synchronize { @stdout += chunk }
417
+ end
418
+
419
+ def append_stderr(chunk)
420
+ @mutex.synchronize { @stderr += chunk }
421
+ end
422
+
423
+ def complete!(exit_value, error_value)
424
+ @mutex.synchronize do
425
+ @completed = true
426
+ @exit_code = parse_exit_code(exit_value)
427
+ @error = error_value if error_value && !error_value.empty?
428
+ end
429
+ end
430
+
431
+ def completed?
432
+ return true if @completed
433
+
434
+ !result_value(:exit_code, "exit_code", "exitCode").nil?
435
+ end
436
+
437
+ def resolved_stdout
438
+ fallback = result_value(:stdout, "stdout")
439
+ @stdout.empty? && fallback ? fallback : @stdout
440
+ end
441
+
442
+ def resolved_stderr
443
+ fallback = result_value(:stderr, "stderr")
444
+ @stderr.empty? && fallback ? fallback : @stderr
445
+ end
446
+
447
+ def resolved_exit_code
448
+ return @exit_code unless @exit_code.nil?
449
+
450
+ raw_exit_code = result_value(:exit_code, "exit_code", "exitCode")
451
+ return nil if raw_exit_code.nil?
452
+
453
+ parse_exit_code(raw_exit_code)
454
+ end
455
+
456
+ def resolved_error
457
+ @error || result_value(:error, "error")
458
+ end
459
+
460
+ def result_value(*keys)
461
+ return nil unless @result.is_a?(Hash)
462
+
463
+ keys.each do |key|
464
+ return @result[key] if @result.key?(key)
465
+ end
466
+
467
+ nil
315
468
  end
316
469
 
317
470
  # Decode a base64-encoded string.
@@ -24,6 +24,7 @@ module E2B
24
24
  # handle = sandbox.commands.run("sleep 10", background: true)
25
25
  # handle.kill
26
26
  class Commands < BaseService
27
+ include LiveStreamable
27
28
  # Run a command in the sandbox
28
29
  #
29
30
  # @param cmd [String] Command to execute (run via /bin/bash -l -c)
@@ -55,8 +56,10 @@ module E2B
55
56
  process_spec[:cwd] = cwd if cwd
56
57
 
57
58
  body = { process: process_spec }
59
+ headers = user_auth_headers(user)
58
60
 
59
61
  # Set up streaming callback
62
+ stream_block = block if block_given?
60
63
  streaming_callback = nil
61
64
  if on_stdout || on_stderr || block_given?
62
65
  streaming_callback = lambda { |event_data|
@@ -66,46 +69,45 @@ module E2B
66
69
  on_stdout&.call(stdout_chunk) if stdout_chunk && !stdout_chunk.empty?
67
70
  on_stderr&.call(stderr_chunk) if stderr_chunk && !stderr_chunk.empty?
68
71
 
69
- if block_given?
70
- yield(:stdout, stdout_chunk) if stdout_chunk && !stdout_chunk.empty?
71
- yield(:stderr, stderr_chunk) if stderr_chunk && !stderr_chunk.empty?
72
- end
72
+ stream_block&.call(:stdout, stdout_chunk) if stdout_chunk && !stdout_chunk.empty?
73
+ stream_block&.call(:stderr, stderr_chunk) if stderr_chunk && !stderr_chunk.empty?
73
74
  }
74
75
  end
75
76
 
76
77
  effective_timeout = request_timeout || (timeout + 30)
77
78
 
79
+ if background
80
+ return build_live_handle(
81
+ rpc_method: "Start",
82
+ body: body,
83
+ timeout: effective_timeout,
84
+ headers: headers,
85
+ on_stdout: on_stdout,
86
+ on_stderr: on_stderr,
87
+ &block
88
+ )
89
+ end
90
+
78
91
  response = envd_rpc("process.Process", "Start",
79
92
  body: body,
80
93
  timeout: effective_timeout,
94
+ headers: headers,
81
95
  on_event: streaming_callback)
82
96
 
83
- pid = extract_pid(response)
97
+ # Return CommandResult for foreground processes
98
+ result = build_result(response)
84
99
 
85
- if background
86
- # Return a CommandHandle for background processes
87
- CommandHandle.new(
88
- pid: pid,
89
- handle_kill: -> { kill(pid) },
90
- handle_send_stdin: ->(data) { send_stdin(pid, data) },
91
- result: response
100
+ # Raise on non-zero exit code (matching official SDK behavior)
101
+ if result.exit_code != 0
102
+ raise CommandExitError.new(
103
+ stdout: result.stdout,
104
+ stderr: result.stderr,
105
+ exit_code: result.exit_code,
106
+ error: result.error
92
107
  )
93
- else
94
- # Return CommandResult for foreground processes
95
- result = build_result(response)
96
-
97
- # Raise on non-zero exit code (matching official SDK behavior)
98
- if result.exit_code != 0
99
- raise CommandExitError.new(
100
- stdout: result.stdout,
101
- stderr: result.stderr,
102
- exit_code: result.exit_code,
103
- error: result.error
104
- )
105
- end
106
-
107
- result
108
108
  end
109
+
110
+ result
109
111
  end
110
112
 
111
113
  # List running processes
@@ -133,12 +135,13 @@ module E2B
133
135
  # @param pid [Integer] Process ID to kill
134
136
  # @param request_timeout [Integer, nil] Request timeout in seconds
135
137
  # @return [Boolean] true if killed, false if not found
136
- def kill(pid, request_timeout: nil)
138
+ def kill(pid, request_timeout: nil, headers: nil)
137
139
  envd_rpc("process.Process", "SendSignal",
138
140
  body: {
139
141
  process: { pid: pid },
140
142
  signal: 9 # SIGKILL
141
143
  },
144
+ headers: headers,
142
145
  timeout: request_timeout || 30)
143
146
  true
144
147
  rescue E2B::NotFoundError
@@ -152,13 +155,14 @@ module E2B
152
155
  # @param pid [Integer] Process ID
153
156
  # @param data [String] Data to send to stdin
154
157
  # @param request_timeout [Integer, nil] Request timeout in seconds
155
- def send_stdin(pid, data, request_timeout: nil)
158
+ def send_stdin(pid, data, request_timeout: nil, headers: nil)
156
159
  encoded = Base64.strict_encode64(data.to_s)
157
160
  envd_rpc("process.Process", "SendInput",
158
161
  body: {
159
162
  process: { pid: pid },
160
163
  input: { stdin: encoded }
161
164
  },
165
+ headers: headers,
162
166
  timeout: request_timeout || 30)
163
167
  end
164
168
 
@@ -169,33 +173,16 @@ module E2B
169
173
  # @param request_timeout [Integer, nil] Request timeout in seconds
170
174
  # @return [CommandHandle] Handle for the connected process
171
175
  def connect(pid, timeout: 60, request_timeout: nil)
172
- response = envd_rpc("process.Process", "Connect",
176
+ build_live_handle(
177
+ rpc_method: "Connect",
173
178
  body: { process: { pid: pid } },
174
- timeout: request_timeout || (timeout + 30))
175
-
176
- CommandHandle.new(
177
- pid: pid,
178
- handle_kill: -> { kill(pid) },
179
- handle_send_stdin: ->(data) { send_stdin(pid, data) },
180
- result: response
179
+ headers: user_auth_headers(nil),
180
+ timeout: request_timeout || (timeout + 30)
181
181
  )
182
182
  end
183
183
 
184
184
  private
185
185
 
186
- # Extract PID from streaming response events
187
- def extract_pid(response)
188
- events = response[:events] || []
189
- events.each do |event|
190
- next unless event.is_a?(Hash) && event["event"]
191
- start_event = event["event"]["Start"] || event["event"]["start"]
192
- if start_event && start_event["pid"]
193
- return start_event["pid"].to_i
194
- end
195
- end
196
- nil
197
- end
198
-
199
186
  # Build CommandResult from response
200
187
  def build_result(response)
201
188
  stdout = response[:stdout] || ""