cogbox 0.190.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE +190 -0
  7. data/README.md +184 -0
  8. data/Rakefile +12 -0
  9. data/lib/cogbox/code_interpreter.rb +360 -0
  10. data/lib/cogbox/cogbox.rb +337 -0
  11. data/lib/cogbox/common/charts.rb +125 -0
  12. data/lib/cogbox/common/code_interpreter.rb +57 -0
  13. data/lib/cogbox/common/code_language.rb +15 -0
  14. data/lib/cogbox/common/cogbox.rb +231 -0
  15. data/lib/cogbox/common/file_system.rb +27 -0
  16. data/lib/cogbox/common/git.rb +20 -0
  17. data/lib/cogbox/common/image.rb +501 -0
  18. data/lib/cogbox/common/process.rb +150 -0
  19. data/lib/cogbox/common/pty.rb +310 -0
  20. data/lib/cogbox/common/resources.rb +40 -0
  21. data/lib/cogbox/common/response.rb +84 -0
  22. data/lib/cogbox/common/snapshot.rb +125 -0
  23. data/lib/cogbox/computer_use.rb +920 -0
  24. data/lib/cogbox/config.rb +110 -0
  25. data/lib/cogbox/file_system.rb +452 -0
  26. data/lib/cogbox/file_transfer.rb +384 -0
  27. data/lib/cogbox/git.rb +335 -0
  28. data/lib/cogbox/lsp_server.rb +140 -0
  29. data/lib/cogbox/object_storage.rb +173 -0
  30. data/lib/cogbox/otel.rb +184 -0
  31. data/lib/cogbox/process.rb +551 -0
  32. data/lib/cogbox/sandbox.rb +752 -0
  33. data/lib/cogbox/sdk/version.rb +11 -0
  34. data/lib/cogbox/sdk.rb +57 -0
  35. data/lib/cogbox/snapshot_service.rb +239 -0
  36. data/lib/cogbox/util.rb +81 -0
  37. data/lib/cogbox/volume.rb +47 -0
  38. data/lib/cogbox/volume_service.rb +62 -0
  39. data/lib/cogbox.rb +11 -0
  40. data/project.json +100 -0
  41. data/scripts/generate-docs.rb +403 -0
  42. data/sig/cogbox/sdk.rbs +6 -0
  43. metadata +240 -0
@@ -0,0 +1,360 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # Copyright Cognifyi
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require 'json'
8
+ require 'websocket-client-simple'
9
+ require 'timeout'
10
+
11
+ module Cogbox
12
+ # Handles code interpretation and execution within a Sandbox. Currently supports only Python.
13
+ #
14
+ # This class provides methods to execute code in isolated interpreter contexts,
15
+ # manage contexts, and stream execution output via callbacks. If subsequent code executions
16
+ # are performed in the same context, the variables, imports, and functions defined in
17
+ # the previous execution will be available.
18
+ #
19
+ # For other languages, use the `code_run` method from the `Process` interface,
20
+ # or execute the appropriate command directly in the sandbox terminal.
21
+ class CodeInterpreter
22
+ include Instrumentation
23
+
24
+ WEBSOCKET_TIMEOUT_CODE = 4008
25
+ WS_PORT = 2280
26
+ private_constant :WS_PORT
27
+
28
+ # @param sandbox_id [String]
29
+ # @param toolbox_api [CogboxToolboxApiClient::InterpreterApi]
30
+ # @param get_preview_link [Proc]
31
+ # @param otel_state [Cogbox::OtelState, nil]
32
+ def initialize(sandbox_id:, toolbox_api:, get_preview_link:, otel_state: nil)
33
+ @sandbox_id = sandbox_id
34
+ @toolbox_api = toolbox_api
35
+ @get_preview_link = get_preview_link
36
+ @otel_state = otel_state
37
+ end
38
+
39
+ # Execute Python code in the sandbox.
40
+ #
41
+ # By default, code runs in the default shared context which persists variables,
42
+ # imports, and functions across executions. To run in an isolated context,
43
+ # create a new context with `create_context` and pass it as the `context` argument.
44
+ #
45
+ # @param code [String] Code to execute
46
+ # @param context [CogboxToolboxApiClient::InterpreterContext, nil] Context to run code in
47
+ # @param on_stdout [Proc, nil] Callback for stdout messages (receives OutputMessage)
48
+ # @param on_stderr [Proc, nil] Callback for stderr messages (receives OutputMessage)
49
+ # @param on_error [Proc, nil] Callback for execution errors (receives ExecutionError)
50
+ # @param envs [Hash<String, String>, nil] Environment variables for this execution
51
+ # @param timeout [Integer, nil] Timeout in seconds. 0 means no timeout. Default is 10 minutes.
52
+ # @return [Cogbox::ExecutionResult]
53
+ # @raise [Cogbox::Sdk::Error]
54
+ #
55
+ # @example
56
+ # def handle_stdout(msg)
57
+ # print "STDOUT: #{msg.output}"
58
+ # end
59
+ #
60
+ # def handle_stderr(msg)
61
+ # print "STDERR: #{msg.output}"
62
+ # end
63
+ #
64
+ # def handle_error(err)
65
+ # puts "ERROR: #{err.name}: #{err.value}"
66
+ # end
67
+ #
68
+ # code = <<~PYTHON
69
+ # import sys
70
+ # import time
71
+ # for i in range(5):
72
+ # print(i)
73
+ # time.sleep(1)
74
+ # sys.stderr.write("Counting done!")
75
+ # PYTHON
76
+ #
77
+ # result = sandbox.code_interpreter.run_code(
78
+ # code,
79
+ # on_stdout: method(:handle_stdout),
80
+ # on_stderr: method(:handle_stderr),
81
+ # on_error: method(:handle_error),
82
+ # timeout: 10
83
+ # )
84
+ def run_code(code, context: nil, on_stdout: nil, on_stderr: nil, on_error: nil, envs: nil, timeout: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
85
+ # Get WebSocket URL via preview link
86
+ preview_link = @get_preview_link.call(WS_PORT)
87
+ url = URI.parse(preview_link.url)
88
+ url.scheme = url.scheme == 'https' ? 'wss' : 'ws'
89
+ url.path = '/process/interpreter/execute'
90
+ ws_url = url.to_s
91
+
92
+ result = ExecutionResult.new
93
+
94
+ # Create request payload
95
+ request = { code: }
96
+ request[:contextId] = context.id if context
97
+ request[:envs] = envs if envs
98
+ request[:timeout] = timeout if timeout
99
+
100
+ # Build headers with preview token
101
+ headers = @toolbox_api.api_client.default_headers.dup.merge(
102
+ 'X-Cogbox-Preview-Token' => preview_link.token,
103
+ 'Content-Type' => 'application/json',
104
+ 'Accept' => 'application/json'
105
+ )
106
+
107
+ completion_queue = Queue.new
108
+ interpreter = self
109
+
110
+ puts "[DEBUG] Connecting to WebSocket: #{ws_url}" if ENV['DEBUG']
111
+
112
+ ws = WebSocket::Client::Simple.connect(ws_url, headers:) do |client|
113
+ client.on :open do
114
+ puts '[DEBUG] WebSocket opened, sending request' if ENV['DEBUG']
115
+ client.send(JSON.dump(request))
116
+ end
117
+
118
+ client.on :message do |msg|
119
+ puts "[DEBUG] Received message (length=#{msg.data.length}): #{msg.data.inspect[0..200]}" if ENV['DEBUG']
120
+
121
+ interpreter.send(:handle_message, msg.data, result, on_stdout, on_stderr, on_error, completion_queue)
122
+ end
123
+
124
+ client.on :error do |e|
125
+ puts "[DEBUG] WebSocket error: #{e.message}" if ENV['DEBUG']
126
+ completion_queue.push({ type: :error, error: e })
127
+ end
128
+
129
+ client.on :close do |e|
130
+ if ENV['DEBUG']
131
+ code = e&.code || 'nil'
132
+ reason = e&.reason || 'nil'
133
+ puts "[DEBUG] WebSocket closed: code=#{code}, reason=#{reason}"
134
+ end
135
+ error_info = interpreter.send(:handle_close, e)
136
+ if error_info
137
+ completion_queue.push({ type: :error_from_close, error: error_info })
138
+ else
139
+ completion_queue.push({ type: :close })
140
+ end
141
+ end
142
+ end
143
+
144
+ no_timeout = timeout.is_a?(Numeric) && timeout <= 0
145
+ max_wait = no_timeout ? nil : (timeout || 600) + 30
146
+ start_time = Time.now
147
+ completion_reason = nil
148
+
149
+ loop do
150
+ if max_wait
151
+ remaining = max_wait - (Time.now - start_time)
152
+ if remaining <= 0
153
+ ws.close
154
+ raise Sdk::TimeoutError,
155
+ 'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
156
+ end
157
+ end
158
+
159
+ completion = completion_queue.pop(timeout: max_wait ? remaining : nil)
160
+
161
+ if completion.nil?
162
+ ws.close
163
+ raise Sdk::TimeoutError,
164
+ 'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
165
+ end
166
+
167
+ puts "[DEBUG] Got completion signal: #{completion[:type]}" if ENV['DEBUG']
168
+
169
+ if completion[:type] == :completed
170
+ completion_reason = :completed
171
+ break
172
+ elsif completion[:type] == :error_from_close
173
+ error_msg = completion[:error]
174
+ if error_msg.include?('timed out') || error_msg.include?('Execution timed out')
175
+ raise Sdk::TimeoutError, error_msg
176
+ end
177
+
178
+ raise Sdk::Error, error_msg
179
+ elsif completion[:type] == :close
180
+ elapsed = Time.now - start_time
181
+ if timeout && timeout > 0 && elapsed >= timeout && elapsed < (timeout + 2)
182
+ raise Sdk::TimeoutError,
183
+ 'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
184
+ end
185
+ completion_reason = :close
186
+ break
187
+ elsif completion[:type] == :error
188
+ unless completion[:error].message.include?('stream closed')
189
+ raise Sdk::Error, "WebSocket error: #{completion[:error].message}"
190
+ end
191
+
192
+ completion_reason = :close
193
+ break
194
+ end
195
+ end
196
+
197
+ ws.close if completion_reason != :close
198
+ sleep 0.05
199
+
200
+ result
201
+ rescue Sdk::Error
202
+ # Re-raise SDK errors as-is
203
+ raise
204
+ rescue StandardError => e
205
+ # Wrap unexpected errors
206
+ raise Sdk::Error, "Failed to run code: #{e.message}"
207
+ end
208
+
209
+ # Create a new isolated interpreter context.
210
+ #
211
+ # Contexts provide isolated execution environments with their own global namespace.
212
+ # Variables, imports, and functions defined in one context don't affect others.
213
+ #
214
+ # @param cwd [String, nil] Working directory for the context
215
+ # @return [CogboxToolboxApiClient::InterpreterContext]
216
+ # @raise [Cogbox::Sdk::Error]
217
+ #
218
+ # @example
219
+ # # Create isolated context
220
+ # ctx = sandbox.code_interpreter.create_context
221
+ #
222
+ # # Execute code in this context
223
+ # sandbox.code_interpreter.run_code("x = 100", context: ctx)
224
+ #
225
+ # # Variable only exists in this context
226
+ # result = sandbox.code_interpreter.run_code("print(x)", context: ctx) # OK
227
+ #
228
+ # # Won't see the variable in default context
229
+ # result = sandbox.code_interpreter.run_code("print(x)") # NameError
230
+ #
231
+ # # Clean up
232
+ # sandbox.code_interpreter.delete_context(ctx)
233
+ def create_context(cwd: nil)
234
+ request = CogboxToolboxApiClient::CreateContextRequest.new(cwd:)
235
+ @toolbox_api.create_interpreter_context(request)
236
+ rescue StandardError => e
237
+ raise Sdk::Error, "Failed to create interpreter context: #{e.message}"
238
+ end
239
+
240
+ # List all user-created interpreter contexts.
241
+ #
242
+ # The default context is not included in this list. Only contexts created
243
+ # via `create_context` are returned.
244
+ #
245
+ # @return [Array<CogboxToolboxApiClient::InterpreterContext>]
246
+ # @raise [Cogbox::Sdk::Error]
247
+ #
248
+ # @example
249
+ # contexts = sandbox.code_interpreter.list_contexts
250
+ # contexts.each do |ctx|
251
+ # puts "Context #{ctx.id}: #{ctx.language} at #{ctx.cwd}"
252
+ # end
253
+ def list_contexts
254
+ response = @toolbox_api.list_interpreter_contexts
255
+ response.contexts || []
256
+ rescue StandardError => e
257
+ raise Sdk::Error, "Failed to list interpreter contexts: #{e.message}"
258
+ end
259
+
260
+ # Delete an interpreter context and shut down all associated processes.
261
+ #
262
+ # This permanently removes the context and all its state (variables, imports, etc.).
263
+ # The default context cannot be deleted.
264
+ #
265
+ # @param context [CogboxToolboxApiClient::InterpreterContext]
266
+ # @return [void]
267
+ # @raise [Cogbox::Sdk::Error]
268
+ #
269
+ # @example
270
+ # ctx = sandbox.code_interpreter.create_context
271
+ # # ... use context ...
272
+ # sandbox.code_interpreter.delete_context(ctx)
273
+ def delete_context(context)
274
+ @toolbox_api.delete_interpreter_context(context.id)
275
+ nil
276
+ rescue StandardError => e
277
+ raise Sdk::Error, "Failed to delete interpreter context: #{e.message}"
278
+ end
279
+
280
+ instrument :run_code, :create_context, :list_contexts, :delete_context,
281
+ component: 'CodeInterpreter'
282
+
283
+ private
284
+
285
+ # @return [Cogbox::OtelState, nil]
286
+ attr_reader :otel_state
287
+
288
+ # @return [Hash<String, String>]
289
+ def build_headers
290
+ headers = {}
291
+ @toolbox_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
292
+ headers
293
+ end
294
+
295
+ # @param data [String]
296
+ # @param result [Cogbox::ExecutionResult]
297
+ # @param on_stdout [Proc, nil]
298
+ # @param on_stderr [Proc, nil]
299
+ # @param on_error [Proc, nil]
300
+ # @param completion_queue [Queue, nil] Queue to signal completion
301
+ # @return [void]
302
+ def handle_message(data, result, on_stdout, on_stderr, on_error, completion_queue = nil) # rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
303
+ # Empty messages are just keepalives or noise, ignore them
304
+ if data.nil? || data.empty?
305
+ puts '[DEBUG] Received empty message, ignoring' if ENV['DEBUG']
306
+ return
307
+ end
308
+
309
+ chunk = JSON.parse(data)
310
+ chunk_type = chunk['type']
311
+
312
+ case chunk_type
313
+ when 'stdout'
314
+ stdout = chunk['text'] || ''
315
+ result.stdout += stdout
316
+ on_stdout&.call(OutputMessage.new(output: stdout))
317
+ when 'stderr'
318
+ stderr = chunk['text'] || ''
319
+ result.stderr += stderr
320
+ on_stderr&.call(OutputMessage.new(output: stderr))
321
+ when 'error'
322
+ error = ExecutionError.new(
323
+ name: chunk['name'] || '',
324
+ value: chunk['value'] || '',
325
+ traceback: chunk['traceback'] || ''
326
+ )
327
+ result.error = error
328
+ on_error&.call(error)
329
+ when 'control'
330
+ control_text = chunk['text'] || ''
331
+ if %w[completed interrupted].include?(control_text)
332
+ puts "[DEBUG] Received control message: #{control_text}" if ENV['DEBUG']
333
+ completion_queue&.push({ type: :completed })
334
+ end
335
+ end
336
+ rescue JSON::ParserError => e
337
+ # Skip malformed messages
338
+ warn "Warning: Failed to parse message: #{e.message}" if ENV['DEBUG']
339
+ end
340
+
341
+ # @param event [Object]
342
+ # @return [void]
343
+ def handle_close(event)
344
+ return nil unless event # Skip if event is nil (manual close)
345
+
346
+ code = event.respond_to?(:code) ? event.code : nil
347
+ reason = event.respond_to?(:reason) ? event.reason : nil
348
+
349
+ if code == WEBSOCKET_TIMEOUT_CODE
350
+ return 'Execution timed out: operation exceeded the configured `timeout`. Provide a larger value if needed.'
351
+ end
352
+
353
+ return nil if code == 1000 || code.nil? # Normal closure or no code
354
+
355
+ detail = reason.to_s.empty? ? 'WebSocket connection closed unexpectedly' : reason.to_s
356
+ detail = "#{detail} (close code #{code})" if code
357
+ detail
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,337 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # Copyright Cognifyi
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require 'json'
8
+ require 'uri'
9
+
10
+ module Cogbox
11
+ class Cogbox
12
+ include Instrumentation
13
+
14
+ # @return [Cogbox::Config]
15
+ attr_reader :config
16
+
17
+ # @return [CogboxApiClient]
18
+ attr_reader :api_client
19
+
20
+ # @return [CogboxApiClient::SandboxApi]
21
+ attr_reader :sandbox_api
22
+
23
+ # @return [Cogbox::VolumeService]
24
+ attr_reader :volume
25
+
26
+ # @return [CogboxApiClient::ObjectStorageApi]
27
+ attr_reader :object_storage_api
28
+
29
+ # @return [CogboxApiClient::SnapshotsApi]
30
+ attr_reader :snapshots_api
31
+
32
+ # @return [Cogbox::SnapshotService]
33
+ attr_reader :snapshot
34
+
35
+ # @param config [Cogbox::Config] Configuration options. Defaults to Cogbox::Config.new
36
+ def initialize(config = Config.new)
37
+ @config = config
38
+ ensure_access_token_defined
39
+
40
+ otel_enabled = config.otel_enabled ||
41
+ config._experimental&.dig('otel_enabled') ||
42
+ config.read_env('COGBOX_OTEL_ENABLED') == 'true' ||
43
+ config.read_env('COGBOX_EXPERIMENTAL_OTEL_ENABLED') == 'true'
44
+ @otel_state = (::Cogbox.init_otel(Sdk::VERSION) if otel_enabled)
45
+
46
+ @api_client = build_api_client
47
+ @sandbox_api = CogboxApiClient::SandboxApi.new(api_client)
48
+ @config_api = CogboxApiClient::ConfigApi.new(api_client)
49
+ @volume = VolumeService.new(CogboxApiClient::VolumesApi.new(api_client), otel_state:)
50
+ @object_storage_api = CogboxApiClient::ObjectStorageApi.new(api_client)
51
+ @snapshots_api = CogboxApiClient::SnapshotsApi.new(api_client)
52
+ @snapshot = SnapshotService.new(snapshots_api:, object_storage_api:, default_region_id: config.target,
53
+ otel_state:)
54
+ end
55
+
56
+ # Shuts down OTel providers, flushing any pending telemetry data.
57
+ #
58
+ # @return [void]
59
+ def close
60
+ ::Cogbox.shutdown_otel(@otel_state)
61
+ @otel_state = nil
62
+ end
63
+
64
+ # Creates a sandbox with the specified parameters
65
+ #
66
+ # @param params [Cogbox::CreateSandboxFromSnapshotParams, Cogbox::CreateSandboxFromImageParams, Nil] Sandbox creation parameters
67
+ # @return [Cogbox::Sandbox] The created sandbox
68
+ # @raise [Cogbox::Sdk::Error] If auto_stop_interval or auto_archive_interval is negative
69
+ def create(params = nil, on_snapshot_create_logs: nil)
70
+ if params.nil?
71
+ params = CreateSandboxFromSnapshotParams.new(language: CodeLanguage::PYTHON)
72
+ elsif params.language.nil?
73
+ params.language = CodeLanguage::PYTHON
74
+ end
75
+
76
+ unless CodeLanguage::ALL.include?(params.language.to_s.to_sym)
77
+ raise ArgumentError,
78
+ "Invalid #{CODE_TOOLBOX_LANGUAGE_LABEL}: #{params.language}. Supported languages: #{CodeLanguage::ALL.join(', ')}"
79
+ end
80
+
81
+ _create(params, on_snapshot_create_logs:)
82
+ end
83
+
84
+ # Deletes a Sandbox.
85
+ #
86
+ # @param sandbox [Cogbox::Sandbox]
87
+ # @return [void]
88
+ def delete(sandbox) = sandbox.delete
89
+
90
+ # Gets a Sandbox by its ID.
91
+ #
92
+ # @param id [String]
93
+ # @return [Cogbox::Sandbox]
94
+ def get(id)
95
+ sandbox_dto = sandbox_api.get_sandbox(id)
96
+ to_sandbox(sandbox_dto:)
97
+ end
98
+
99
+ # Iterates over Sandboxes matching the given query.
100
+ #
101
+ # @param query [Cogbox::ListSandboxesQuery, nil] Optional filters, sorting, and per-page size.
102
+ # @return [Enumerator<Cogbox::Sandbox>]
103
+ # @raise [Cogbox::Sdk::Error]
104
+ #
105
+ # @example
106
+ # cogbox.list(Cogbox::ListSandboxesQuery.new(labels: { 'env' => 'dev' })).each do |sandbox|
107
+ # puts sandbox.id
108
+ # end
109
+ def list(query = nil)
110
+ q = query || ListSandboxesQuery.new
111
+
112
+ Enumerator.new do |yielder|
113
+ cursor = nil
114
+ first_page = true
115
+ while first_page || cursor
116
+ first_page = false
117
+ response = fetch_sandbox_page(q, cursor)
118
+ response.items.each do |sandbox_dto|
119
+ yielder << to_sandbox(sandbox_dto: sandbox_dto)
120
+ end
121
+ cursor = response.next_cursor
122
+ break if cursor.nil? || (cursor.respond_to?(:empty?) && cursor.empty?)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Starts a Sandbox and waits for it to be ready.
128
+ #
129
+ # @param sandbox [Cogbox::Sandbox]
130
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
131
+ # @return [void]
132
+ def start(sandbox, timeout = Sandbox::DEFAULT_TIMEOUT) = sandbox.start(timeout)
133
+
134
+ # Stops a Sandbox and waits for it to be stopped.
135
+ #
136
+ # @param sandbox [Cogbox::Sandbox]
137
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
138
+ # @return [void]
139
+ def stop(sandbox, timeout = Sandbox::DEFAULT_TIMEOUT) = sandbox.stop(timeout)
140
+
141
+ instrument :create, :delete, :get, :start, :stop, component: 'Cogbox'
142
+
143
+ private
144
+
145
+ # @return [Cogbox::OtelState, nil]
146
+ attr_reader :otel_state
147
+
148
+ # Fetches a single page of sandboxes. Each call produces one OTel span
149
+ # ("Cogbox.list_fetch_page") so that paginated iteration emits N spans
150
+ # for N pages.
151
+ #
152
+ # @param q [Cogbox::ListSandboxesQuery]
153
+ # @param cursor [String, nil]
154
+ # @return [CogboxApiClient::ListSandboxesResponse]
155
+ def fetch_sandbox_page(q, cursor)
156
+ opts = {
157
+ cursor: cursor,
158
+ limit: q.limit,
159
+ id: q.id,
160
+ name: q.name,
161
+ labels: q.labels ? JSON.dump(q.labels) : nil,
162
+ states: q.states,
163
+ snapshots: q.snapshots,
164
+ region_ids: q.targets,
165
+ min_cpu: q.min_cpu,
166
+ max_cpu: q.max_cpu,
167
+ min_memory_gi_b: q.min_memory_gib,
168
+ max_memory_gi_b: q.max_memory_gib,
169
+ min_disk_gi_b: q.min_disk_gib,
170
+ max_disk_gi_b: q.max_disk_gib,
171
+ is_public: q.is_public,
172
+ is_recoverable: q.is_recoverable,
173
+ created_at_after: q.created_at_after,
174
+ created_at_before: q.created_at_before,
175
+ last_event_after: q.last_activity_after,
176
+ last_event_before: q.last_activity_before,
177
+ sort: q.sort,
178
+ order: q.order
179
+ }.compact
180
+
181
+ sandbox_api.list_sandboxes(opts)
182
+ end
183
+
184
+ instrument :fetch_sandbox_page, component: 'Cogbox.list'
185
+
186
+ # Creates a sandbox with the specified parameters
187
+ #
188
+ # @param params [Cogbox::CreateSandboxFromSnapshotParams, Cogbox::CreateSandboxFromImageParams] Sandbox creation parameters
189
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
190
+ # @param on_snapshot_create_logs [Proc]
191
+ # @return [Cogbox::Sandbox] The created sandbox
192
+ # @raise [Cogbox::Sdk::Error] If auto_stop_interval or auto_archive_interval is negative
193
+ def _create(params, timeout: 60, on_snapshot_create_logs: nil)
194
+ raise Sdk::Error, 'Timeout must be a non-negative number' if timeout.negative?
195
+
196
+ start_time = Time.now
197
+
198
+ raise Sdk::Error, 'auto_stop_interval must be a non-negative integer' if params.auto_stop_interval&.negative?
199
+
200
+ if params.auto_archive_interval&.negative?
201
+ raise Sdk::Error, 'auto_archive_interval must be a non-negative integer'
202
+ end
203
+
204
+ labels = params.labels&.dup || {}
205
+ labels[CODE_TOOLBOX_LANGUAGE_LABEL] = params.language.to_s if params.language
206
+
207
+ create_sandbox = CogboxApiClient::CreateSandbox.new(
208
+ user: params.os_user,
209
+ env: params.env_vars || {},
210
+ labels: labels,
211
+ public: params.public,
212
+ target: config.target,
213
+ auto_stop_interval: params.auto_stop_interval,
214
+ auto_archive_interval: params.auto_archive_interval,
215
+ auto_delete_interval: params.auto_delete_interval,
216
+ volumes: params.volumes,
217
+ network_block_all: params.network_block_all,
218
+ network_allow_list: params.network_allow_list,
219
+ domain_allow_list: params.domain_allow_list,
220
+ linked_sandbox: params.linked_sandbox
221
+ )
222
+
223
+ create_sandbox.snapshot = params.snapshot if params.respond_to?(:snapshot)
224
+
225
+ if params.respond_to?(:image) && params.image.is_a?(String)
226
+ create_sandbox.build_info = CogboxApiClient::CreateBuildInfo.new(
227
+ dockerfile_content: Image.base(params.image).dockerfile
228
+ )
229
+ elsif params.respond_to?(:image) && params.image.is_a?(Image)
230
+ create_sandbox.build_info = CogboxApiClient::CreateBuildInfo.new(
231
+ context_hashes: SnapshotService.process_image_context(object_storage_api, params.image),
232
+ dockerfile_content: params.image.dockerfile
233
+ )
234
+ end
235
+
236
+ if params.respond_to?(:resources)
237
+ create_sandbox.cpu = params.resources&.cpu
238
+ create_sandbox.memory = params.resources&.memory
239
+ create_sandbox.disk = params.resources&.disk
240
+ create_sandbox.gpu = params.resources&.gpu
241
+ if params.resources&.gpu_type
242
+ create_sandbox.gpu_type =
243
+ params.resources.gpu_type.is_a?(Array) ? params.resources.gpu_type : [params.resources.gpu_type]
244
+ end
245
+ end
246
+
247
+ response = sandbox_api.create_sandbox(create_sandbox)
248
+
249
+ if response.state == CogboxApiClient::SandboxState::PENDING_BUILD && on_snapshot_create_logs
250
+ # Wait for state to change from PENDING_BUILD before fetching logs
251
+ while response.state == CogboxApiClient::SandboxState::PENDING_BUILD
252
+ sleep(1)
253
+ response = sandbox_api.get_sandbox(response.id)
254
+ end
255
+
256
+ # Get build logs URL from API
257
+ build_logs_response = sandbox_api.get_build_logs_url(response.id)
258
+ uri = URI.parse("#{build_logs_response.url}?follow=true")
259
+
260
+ headers = {}
261
+ sandbox_api.api_client.update_params_for_auth!(headers, nil, ['bearer'])
262
+ Util.stream_async(uri:, headers:, on_chunk: on_snapshot_create_logs)
263
+ end
264
+
265
+ sandbox = to_sandbox(sandbox_dto: response)
266
+
267
+ if sandbox.state != CogboxApiClient::SandboxState::STARTED
268
+ sandbox.wait_for_sandbox_start([0.001, timeout - (Time.now - start_time)].max)
269
+ end
270
+
271
+ sandbox
272
+ end
273
+
274
+ # @return [void]
275
+ # @raise [Cogbox::Sdk::Error]
276
+ def ensure_access_token_defined
277
+ return if config.api_key
278
+
279
+ unless config.jwt_token
280
+ raise Sdk::Error,
281
+ 'Authentication credentials not found. Set COGBOX_API_KEY, or both COGBOX_JWT_TOKEN and ' \
282
+ 'COGBOX_ORGANIZATION_ID. These can also be provided via Cogbox::Config.'
283
+ end
284
+ return if config.organization_id
285
+
286
+ raise Sdk::Error,
287
+ 'COGBOX_ORGANIZATION_ID is required when authenticating with COGBOX_JWT_TOKEN. ' \
288
+ 'It can also be provided via Cogbox::Config.'
289
+ end
290
+
291
+ # @return [CogboxApiClient::ApiClient]
292
+ def build_api_client
293
+ CogboxApiClient::ApiClient.new(api_client_config).tap do |client|
294
+ client.default_headers[HEADER_SOURCE] = SOURCE_RUBY
295
+ client.default_headers[HEADER_SDK_VERSION] = Sdk::VERSION
296
+ client.default_headers[HEADER_ORGANIZATION_ID] = config.organization_id if config.jwt_token
297
+ client.user_agent = "sdk-ruby/#{Sdk::VERSION}"
298
+ end
299
+ end
300
+
301
+ # @return [CogboxApiClient::Configuration]
302
+ def api_client_config
303
+ CogboxApiClient::Configuration.new.configure do |api_config|
304
+ uri = URI(config.api_url)
305
+ api_config.scheme = uri.scheme
306
+ api_config.host = uri.authority # Includes hostname:port
307
+ api_config.base_path = uri.path
308
+
309
+ api_config.access_token_getter = proc { config.api_key || config.jwt_token }
310
+ api_config
311
+ end
312
+ end
313
+
314
+ # @param sandbox_dto [CogboxApiClient::Sandbox, CogboxApiClient::SandboxListItem]
315
+ # @return [Cogbox::Sandbox]
316
+ def to_sandbox(sandbox_dto:)
317
+ Sandbox.new(
318
+ sandbox_dto:,
319
+ config:,
320
+ sandbox_api:,
321
+ otel_state: @otel_state
322
+ )
323
+ end
324
+
325
+ SOURCE_RUBY = 'sdk-ruby'
326
+ private_constant :SOURCE_RUBY
327
+
328
+ HEADER_SOURCE = 'X-Cogbox-Source'
329
+ private_constant :HEADER_SOURCE
330
+
331
+ HEADER_SDK_VERSION = 'X-Cogbox-SDK-Version'
332
+ private_constant :HEADER_SDK_VERSION
333
+
334
+ HEADER_ORGANIZATION_ID = 'X-Cogbox-Organization-ID'
335
+ private_constant :HEADER_ORGANIZATION_ID
336
+ end
337
+ end