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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +190 -0
- data/README.md +184 -0
- data/Rakefile +12 -0
- data/lib/cogbox/code_interpreter.rb +360 -0
- data/lib/cogbox/cogbox.rb +337 -0
- data/lib/cogbox/common/charts.rb +125 -0
- data/lib/cogbox/common/code_interpreter.rb +57 -0
- data/lib/cogbox/common/code_language.rb +15 -0
- data/lib/cogbox/common/cogbox.rb +231 -0
- data/lib/cogbox/common/file_system.rb +27 -0
- data/lib/cogbox/common/git.rb +20 -0
- data/lib/cogbox/common/image.rb +501 -0
- data/lib/cogbox/common/process.rb +150 -0
- data/lib/cogbox/common/pty.rb +310 -0
- data/lib/cogbox/common/resources.rb +40 -0
- data/lib/cogbox/common/response.rb +84 -0
- data/lib/cogbox/common/snapshot.rb +125 -0
- data/lib/cogbox/computer_use.rb +920 -0
- data/lib/cogbox/config.rb +110 -0
- data/lib/cogbox/file_system.rb +452 -0
- data/lib/cogbox/file_transfer.rb +384 -0
- data/lib/cogbox/git.rb +335 -0
- data/lib/cogbox/lsp_server.rb +140 -0
- data/lib/cogbox/object_storage.rb +173 -0
- data/lib/cogbox/otel.rb +184 -0
- data/lib/cogbox/process.rb +551 -0
- data/lib/cogbox/sandbox.rb +752 -0
- data/lib/cogbox/sdk/version.rb +11 -0
- data/lib/cogbox/sdk.rb +57 -0
- data/lib/cogbox/snapshot_service.rb +239 -0
- data/lib/cogbox/util.rb +81 -0
- data/lib/cogbox/volume.rb +47 -0
- data/lib/cogbox/volume_service.rb +62 -0
- data/lib/cogbox.rb +11 -0
- data/project.json +100 -0
- data/scripts/generate-docs.rb +403 -0
- data/sig/cogbox/sdk.rbs +6 -0
- 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
|