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,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require_relative "base_service"
5
+ require_relative "command_handle"
6
+
7
+ module E2B
8
+ module Services
9
+ # Pseudo-terminal size specification.
10
+ #
11
+ # @example Default 80x24 terminal
12
+ # size = PtySize.new
13
+ #
14
+ # @example Custom size
15
+ # size = PtySize.new(cols: 120, rows: 40)
16
+ class PtySize
17
+ # @return [Integer] Number of columns
18
+ attr_reader :cols
19
+
20
+ # @return [Integer] Number of rows
21
+ attr_reader :rows
22
+
23
+ # @param cols [Integer] Number of columns (default: 80)
24
+ # @param rows [Integer] Number of rows (default: 24)
25
+ def initialize(cols: 80, rows: 24)
26
+ @cols = cols
27
+ @rows = rows
28
+ end
29
+
30
+ # Convert to a Hash suitable for the Connect RPC request body.
31
+ #
32
+ # @return [Hash]
33
+ def to_h
34
+ { cols: @cols, rows: @rows }
35
+ end
36
+ end
37
+
38
+ # PTY (pseudo-terminal) service for E2B sandbox.
39
+ #
40
+ # Provides methods to create, connect to, and manage interactive
41
+ # pseudo-terminals inside the sandbox. Uses Connect RPC protocol
42
+ # with the +process.Process+ service.
43
+ #
44
+ # @example Create a PTY and send commands
45
+ # pty = sandbox.pty
46
+ # handle = pty.create
47
+ # handle.send_stdin("ls -la\n")
48
+ # result = handle.wait(on_pty: ->(data) { print data })
49
+ #
50
+ # @example Resize a PTY
51
+ # pty.resize(handle.pid, PtySize.new(cols: 120, rows: 40))
52
+ #
53
+ # @example Connect to an existing PTY
54
+ # handle = pty.connect(pid)
55
+ class Pty < BaseService
56
+ # Default shell to use for PTY sessions
57
+ DEFAULT_SHELL = "/bin/bash"
58
+
59
+ # Default shell arguments for interactive login shell
60
+ DEFAULT_SHELL_ARGS = ["-i", "-l"].freeze
61
+
62
+ # Create a new PTY (pseudo-terminal) session in the sandbox.
63
+ #
64
+ # Starts an interactive shell process with a PTY attached. The
65
+ # returned {CommandHandle} can be used to send input, receive
66
+ # output, and manage the PTY lifecycle.
67
+ #
68
+ # @param size [PtySize] Terminal size (default: 80 columns x 24 rows)
69
+ # @param user [String, nil] User to run the PTY as
70
+ # @param cwd [String, nil] Working directory for the PTY shell
71
+ # @param envs [Hash{String => String}, nil] Environment variables
72
+ # @param cmd [String] Shell executable (default: /bin/bash)
73
+ # @param args [Array<String>] Shell arguments (default: ["-i", "-l"])
74
+ # @param timeout [Integer] Timeout for the PTY session in seconds
75
+ # @return [CommandHandle] Handle to interact with the PTY
76
+ # @raise [E2B::E2BError] if the PTY could not be started
77
+ #
78
+ # @example
79
+ # handle = sandbox.pty.create(
80
+ # size: PtySize.new(cols: 120, rows: 40),
81
+ # cwd: "/home/user/project",
82
+ # envs: { "EDITOR" => "vim" }
83
+ # )
84
+ def create(size: PtySize.new, user: nil, cwd: nil, envs: nil,
85
+ cmd: DEFAULT_SHELL, args: DEFAULT_SHELL_ARGS, timeout: 60)
86
+ envs = build_pty_envs(envs)
87
+
88
+ process_spec = {
89
+ cmd: cmd,
90
+ args: args,
91
+ envs: envs
92
+ }
93
+ process_spec[:cwd] = cwd if cwd
94
+
95
+ body = {
96
+ process: process_spec,
97
+ pty: {
98
+ size: size.to_h
99
+ }
100
+ }
101
+
102
+ pid = nil
103
+
104
+ # Use streaming RPC to capture the StartEvent and extract the PID
105
+ on_event = ->(event_data) {
106
+ event = event_data[:event]
107
+ if event.is_a?(Hash) && event["event"]
108
+ start_event = event["event"]["Start"] || event["event"]["start"]
109
+ if start_event && start_event["pid"]
110
+ pid = start_event["pid"]
111
+ end
112
+ end
113
+ }
114
+
115
+ response = envd_rpc(
116
+ "process.Process", "Start",
117
+ body: body,
118
+ timeout: timeout + 30,
119
+ on_event: on_event
120
+ )
121
+
122
+ # If PID was not captured from streaming, try the accumulated result
123
+ pid ||= extract_pid_from_result(response)
124
+
125
+ CommandHandle.new(
126
+ pid: pid,
127
+ handle_kill: -> { kill(pid) },
128
+ handle_send_stdin: ->(data) { send_stdin(pid, data) },
129
+ result: response
130
+ )
131
+ end
132
+
133
+ # Connect to an existing PTY process.
134
+ #
135
+ # Attaches to a running PTY process by PID and returns a handle
136
+ # for sending input and receiving output.
137
+ #
138
+ # @param pid [Integer] Process ID of the PTY to connect to
139
+ # @param timeout [Integer] Timeout for the connection in seconds
140
+ # @return [CommandHandle] Handle to interact with the PTY
141
+ # @raise [E2B::E2BError] if the process is not found or connection fails
142
+ #
143
+ # @example
144
+ # handle = sandbox.pty.connect(12345)
145
+ # handle.send_stdin("whoami\n")
146
+ def connect(pid, timeout: 60)
147
+ body = {
148
+ process: { pid: pid }
149
+ }
150
+
151
+ response = envd_rpc(
152
+ "process.Process", "Connect",
153
+ body: body,
154
+ timeout: timeout + 30
155
+ )
156
+
157
+ CommandHandle.new(
158
+ pid: pid,
159
+ handle_kill: -> { kill(pid) },
160
+ handle_send_stdin: ->(data) { send_stdin(pid, data) },
161
+ result: response
162
+ )
163
+ end
164
+
165
+ # Send input data to a PTY.
166
+ #
167
+ # The data is base64-encoded and sent as PTY input (not stdin),
168
+ # which means it goes through the terminal emulator and supports
169
+ # control characters, escape sequences, etc.
170
+ #
171
+ # @param pid [Integer] Process ID of the PTY
172
+ # @param data [String] Input data to send (e.g., "ls -la\n")
173
+ # @return [void]
174
+ # @raise [E2B::E2BError] if the process is not found
175
+ #
176
+ # @example Send a command
177
+ # sandbox.pty.send_stdin(pid, "echo hello\n")
178
+ #
179
+ # @example Send Ctrl+C
180
+ # sandbox.pty.send_stdin(pid, "\x03")
181
+ def send_stdin(pid, data)
182
+ encoded = Base64.strict_encode64(data.is_a?(String) ? data : data.to_s)
183
+ envd_rpc("process.Process", "SendInput", body: {
184
+ process: { pid: pid },
185
+ input: { pty: encoded }
186
+ })
187
+ end
188
+
189
+ # Kill a PTY process with SIGKILL.
190
+ #
191
+ # @param pid [Integer] Process ID of the PTY to kill
192
+ # @return [Boolean] true if the signal was sent, false if the process was not found
193
+ #
194
+ # @example
195
+ # sandbox.pty.kill(12345)
196
+ def kill(pid)
197
+ envd_rpc("process.Process", "SendSignal", body: {
198
+ process: { pid: pid },
199
+ signal: 9 # SIGKILL
200
+ })
201
+ true
202
+ rescue E2B::E2BError
203
+ false
204
+ end
205
+
206
+ # Resize a PTY terminal.
207
+ #
208
+ # Should be called when the terminal window size changes to keep
209
+ # the remote PTY in sync.
210
+ #
211
+ # @param pid [Integer] Process ID of the PTY
212
+ # @param size [PtySize] New terminal size
213
+ # @return [void]
214
+ # @raise [E2B::E2BError] if the process is not found
215
+ #
216
+ # @example
217
+ # sandbox.pty.resize(pid, PtySize.new(cols: 120, rows: 40))
218
+ def resize(pid, size)
219
+ envd_rpc("process.Process", "Update", body: {
220
+ process: { pid: pid },
221
+ pty: {
222
+ size: size.to_h
223
+ }
224
+ })
225
+ end
226
+
227
+ # Close the stdin of a PTY process.
228
+ #
229
+ # After calling this, no more input can be sent to the PTY via
230
+ # {#send_stdin}.
231
+ #
232
+ # @param pid [Integer] Process ID of the PTY
233
+ # @return [void]
234
+ # @raise [E2B::E2BError] if the process is not found
235
+ def close_stdin(pid)
236
+ envd_rpc("process.Process", "CloseStdin", body: {
237
+ process: { pid: pid }
238
+ })
239
+ end
240
+
241
+ # List running processes in the sandbox.
242
+ #
243
+ # @return [Array<Hash>] List of running process descriptors
244
+ def list
245
+ response = envd_rpc("process.Process", "List", body: {})
246
+ response["processes"] || response[:processes] || []
247
+ end
248
+
249
+ private
250
+
251
+ # Build environment variables hash with PTY defaults.
252
+ #
253
+ # Ensures TERM, LANG, and LC_ALL are set to sensible defaults
254
+ # for terminal operation unless the caller has overridden them.
255
+ #
256
+ # @param envs [Hash{String => String}, nil] Caller-provided env vars
257
+ # @return [Hash{String => String}]
258
+ def build_pty_envs(envs)
259
+ result = {}
260
+ result["TERM"] = "xterm-256color"
261
+ result["LANG"] = "C.UTF-8"
262
+ result["LC_ALL"] = "C.UTF-8"
263
+
264
+ if envs.is_a?(Hash)
265
+ envs.each { |k, v| result[k.to_s] = v.to_s }
266
+ end
267
+
268
+ result
269
+ end
270
+
271
+ # Extract PID from the StartEvent in a pre-materialized RPC result.
272
+ #
273
+ # The result hash from {EnvdHttpClient#handle_streaming_rpc} or
274
+ # {EnvdHttpClient#handle_rpc_response} contains an :events array.
275
+ # The first event with a Start sub-event carries the PID.
276
+ #
277
+ # @param response [Hash] RPC response hash with :events key
278
+ # @return [Integer, nil] Process ID, or nil if not found
279
+ def extract_pid_from_result(response)
280
+ return nil unless response.is_a?(Hash)
281
+
282
+ events = response[:events] || []
283
+ events.each do |event_hash|
284
+ next unless event_hash.is_a?(Hash) && event_hash["event"]
285
+
286
+ event = event_hash["event"]
287
+ start_event = event["Start"] || event["start"]
288
+ if start_event && start_event["pid"]
289
+ return start_event["pid"]
290
+ end
291
+ end
292
+
293
+ nil
294
+ end
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E2B
4
+ module Services
5
+ # Handle for watching directory changes in the sandbox
6
+ #
7
+ # Returned by {Filesystem#watch_dir}. Uses the polling-based watcher RPCs
8
+ # (CreateWatcher/GetWatcherEvents/RemoveWatcher) from the filesystem proto service.
9
+ #
10
+ # The watcher is created externally and its ID is passed into this handle.
11
+ # Call {#get_new_events} to poll for new filesystem changes, and {#stop}
12
+ # to clean up the watcher when done.
13
+ #
14
+ # @example Basic usage
15
+ # handle = sandbox.files.watch_dir("/home/user/project")
16
+ # loop do
17
+ # events = handle.get_new_events
18
+ # events.each { |e| puts "#{e.name}: #{e.type}" }
19
+ # sleep 1
20
+ # end
21
+ # handle.stop
22
+ #
23
+ # @example With ensure block for cleanup
24
+ # handle = sandbox.files.watch_dir("/home/user/project")
25
+ # begin
26
+ # events = handle.get_new_events
27
+ # events.each { |e| process_event(e) }
28
+ # ensure
29
+ # handle.stop
30
+ # end
31
+ class WatchHandle
32
+ # @return [String] The watcher ID assigned by the CreateWatcher RPC
33
+ attr_reader :watcher_id
34
+
35
+ # Create a new WatchHandle
36
+ #
37
+ # @param watcher_id [String] The watcher ID returned by the CreateWatcher RPC
38
+ # @param envd_rpc_proc [Proc] A callable that performs RPC calls. It must accept
39
+ # three positional arguments (service, method) and keyword arguments (body:, timeout:).
40
+ # Typically a lambda wrapping {BaseService#envd_rpc}.
41
+ def initialize(watcher_id:, envd_rpc_proc:)
42
+ @watcher_id = watcher_id
43
+ @envd_rpc_proc = envd_rpc_proc
44
+ @stopped = false
45
+ end
46
+
47
+ # Poll for new filesystem events since the last check
48
+ #
49
+ # Calls the GetWatcherEvents RPC to retrieve any filesystem events
50
+ # that have occurred since the last poll (or since the watcher was created).
51
+ #
52
+ # @return [Array<Models::FilesystemEvent>] New events since last poll
53
+ # @raise [E2B::E2BError] If the watcher has been stopped
54
+ def get_new_events
55
+ raise E2B::E2BError, "Watcher has been stopped" if @stopped
56
+
57
+ response = @envd_rpc_proc.call(
58
+ "filesystem.Filesystem", "GetWatcherEvents",
59
+ body: { watcherId: @watcher_id }
60
+ )
61
+
62
+ events = extract_events(response)
63
+ events.map { |e| Models::FilesystemEvent.from_hash(e) }
64
+ end
65
+
66
+ # Stop watching and clean up the watcher
67
+ #
68
+ # Calls the RemoveWatcher RPC to release server-side resources.
69
+ # After calling this method, {#get_new_events} will raise an error.
70
+ # Calling stop on an already-stopped handle is a no-op.
71
+ #
72
+ # @return [void]
73
+ def stop
74
+ return if @stopped
75
+
76
+ @envd_rpc_proc.call(
77
+ "filesystem.Filesystem", "RemoveWatcher",
78
+ body: { watcherId: @watcher_id }
79
+ )
80
+ @stopped = true
81
+ rescue StandardError
82
+ @stopped = true
83
+ # Ignore errors on cleanup - the watcher may have already been
84
+ # removed server-side (e.g., sandbox shutdown)
85
+ end
86
+
87
+ # Check if the watcher has been stopped
88
+ #
89
+ # @return [Boolean] true if {#stop} has been called
90
+ def stopped?
91
+ @stopped
92
+ end
93
+
94
+ private
95
+
96
+ # Extract events array from the RPC response
97
+ #
98
+ # The response may contain events under different keys depending on
99
+ # the serialization format (camelCase JSON vs. symbol keys from parsed response).
100
+ #
101
+ # @param response [Hash] The parsed RPC response
102
+ # @return [Array<Hash>] Array of raw event hashes
103
+ def extract_events(response)
104
+ return [] unless response.is_a?(Hash)
105
+
106
+ response["events"] || response[:events] || []
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E2B
4
+ VERSION = "0.2.0"
5
+ end
data/lib/e2b.rb ADDED
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # E2B Ruby SDK
4
+ # Ruby client for E2B sandbox API
5
+
6
+ # Core requires
7
+ require_relative "e2b/version"
8
+ require_relative "e2b/errors"
9
+ require_relative "e2b/configuration"
10
+
11
+ # API layer
12
+ require_relative "e2b/api/http_client"
13
+
14
+ # Models
15
+ require_relative "e2b/models/sandbox_info"
16
+ require_relative "e2b/models/process_result"
17
+ require_relative "e2b/models/entry_info"
18
+
19
+ # Services
20
+ require_relative "e2b/services/base_service"
21
+ require_relative "e2b/services/command_handle"
22
+ require_relative "e2b/services/commands"
23
+ require_relative "e2b/services/filesystem"
24
+ require_relative "e2b/services/watch_handle"
25
+ require_relative "e2b/services/pty"
26
+ require_relative "e2b/services/git"
27
+
28
+ # Core classes
29
+ require_relative "e2b/sandbox"
30
+ require_relative "e2b/client"
31
+
32
+ # E2B SDK for Ruby
33
+ #
34
+ # Provides access to E2B sandboxes - secure cloud environments
35
+ # for AI-generated code execution.
36
+ #
37
+ # @example Quick start with Sandbox class (recommended)
38
+ # sandbox = E2B::Sandbox.create(template: "base", api_key: "your-key")
39
+ #
40
+ # result = sandbox.commands.run("echo 'Hello, World!'")
41
+ # puts result.stdout
42
+ #
43
+ # sandbox.files.write("/home/user/hello.txt", "Hello!")
44
+ # content = sandbox.files.read("/home/user/hello.txt")
45
+ #
46
+ # sandbox.kill
47
+ #
48
+ # @example Using Client class
49
+ # client = E2B::Client.new(api_key: "your-api-key")
50
+ # sandbox = client.create(template: "base")
51
+ #
52
+ # @example Using global configuration
53
+ # E2B.configure do |config|
54
+ # config.api_key = "your-api-key"
55
+ # end
56
+ #
57
+ # sandbox = E2B::Sandbox.create(template: "base")
58
+ #
59
+ # @see https://e2b.dev/docs E2B Documentation
60
+ module E2B
61
+ class << self
62
+ # @return [Configuration, nil] Global configuration
63
+ attr_accessor :configuration
64
+
65
+ # Configure the E2B SDK globally
66
+ #
67
+ # @yield [config] Configuration block
68
+ # @yieldparam config [Configuration] Configuration instance
69
+ # @return [Configuration]
70
+ #
71
+ # @example
72
+ # E2B.configure do |config|
73
+ # config.api_key = "your-api-key"
74
+ # config.domain = "e2b.app"
75
+ # end
76
+ def configure
77
+ self.configuration ||= Configuration.new
78
+ yield(configuration) if block_given?
79
+ configuration
80
+ end
81
+
82
+ # Reset global configuration
83
+ def reset_configuration!
84
+ self.configuration = nil
85
+ end
86
+ end
87
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: e2b
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Tao Luo
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-03-12 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: faraday
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '3.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: faraday-multipart
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.0'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '13.0'
53
+ type: :development
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '13.0'
60
+ - !ruby/object:Gem::Dependency
61
+ name: rspec
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3.0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '3.0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: webmock
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '3.0'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '3.0'
88
+ description: |
89
+ Ruby client for creating and managing E2B sandboxes - secure cloud environments
90
+ for AI-generated code execution. Supports sandbox lifecycle management, command
91
+ execution, file operations, PTY terminals, git operations, and directory watching.
92
+ email:
93
+ - luotao@hey.com
94
+ executables: []
95
+ extensions: []
96
+ extra_rdoc_files: []
97
+ files:
98
+ - LICENSE.txt
99
+ - README.md
100
+ - lib/e2b.rb
101
+ - lib/e2b/api/http_client.rb
102
+ - lib/e2b/client.rb
103
+ - lib/e2b/configuration.rb
104
+ - lib/e2b/errors.rb
105
+ - lib/e2b/models/entry_info.rb
106
+ - lib/e2b/models/process_result.rb
107
+ - lib/e2b/models/sandbox_info.rb
108
+ - lib/e2b/sandbox.rb
109
+ - lib/e2b/services/base_service.rb
110
+ - lib/e2b/services/command_handle.rb
111
+ - lib/e2b/services/commands.rb
112
+ - lib/e2b/services/filesystem.rb
113
+ - lib/e2b/services/git.rb
114
+ - lib/e2b/services/pty.rb
115
+ - lib/e2b/services/watch_handle.rb
116
+ - lib/e2b/version.rb
117
+ homepage: https://github.com/ya-luotao/e2b-ruby
118
+ licenses:
119
+ - MIT
120
+ metadata:
121
+ homepage_uri: https://github.com/ya-luotao/e2b-ruby
122
+ source_code_uri: https://github.com/ya-luotao/e2b-ruby
123
+ documentation_uri: https://e2b.dev/docs
124
+ changelog_uri: https://github.com/ya-luotao/e2b-ruby/blob/main/CHANGELOG.md
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: 3.0.0
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubygems_version: 3.6.2
140
+ specification_version: 4
141
+ summary: Ruby SDK for E2B sandbox API
142
+ test_files: []