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,752 @@
1
+ # Copyright Daytona Platforms Inc.
2
+ # Copyright Cognifyi
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # frozen_string_literal: true
6
+
7
+ require 'timeout'
8
+
9
+ module Cogbox
10
+ class Sandbox # rubocop:disable Metrics/ClassLength
11
+ include Instrumentation
12
+
13
+ DEFAULT_TIMEOUT = 60
14
+
15
+ # @return [String] The ID of the sandbox
16
+ attr_reader :id
17
+
18
+ # @return [String] The organization ID of the sandbox
19
+ attr_reader :organization_id
20
+
21
+ # @return [String] The snapshot used for the sandbox
22
+ attr_reader :snapshot
23
+
24
+ # @return [String] The user associated with the project
25
+ attr_reader :user
26
+
27
+ # @return [Hash<String, String>, nil] Environment variables for the sandbox.
28
+ # Not returned by list results; call #refresh on each item to populate.
29
+ attr_reader :env
30
+
31
+ # @return [Hash<String, String>] Labels for the sandbox
32
+ attr_reader :labels
33
+
34
+ # @return [Boolean] Whether the sandbox http preview is public
35
+ attr_reader :public
36
+
37
+ # @return [Boolean, nil] Whether to block all network access for the sandbox.
38
+ # Not returned by list results; call #refresh on each item to populate.
39
+ attr_reader :network_block_all
40
+
41
+ # @return [String, nil] Comma-separated list of allowed CIDR network addresses for the sandbox.
42
+ # Not returned by list results; call #refresh on each item to populate.
43
+ attr_reader :network_allow_list
44
+
45
+ # @return [String, nil] Comma-separated list of allowed domains for the sandbox.
46
+ # Not returned by list results; call #refresh on each item to populate.
47
+ attr_reader :domain_allow_list
48
+
49
+ # @return [String] The target environment for the sandbox
50
+ attr_reader :target
51
+
52
+ # @return [Float] The CPU quota for the sandbox
53
+ attr_reader :cpu
54
+
55
+ # @return [Float] The GPU quota for the sandbox
56
+ attr_reader :gpu
57
+
58
+ # @return [Float] The memory quota for the sandbox
59
+ attr_reader :memory
60
+
61
+ # @return [Float] The disk quota for the sandbox
62
+ attr_reader :disk
63
+
64
+ # @return [CogboxApiClient::SandboxState] The state of the sandbox
65
+ attr_reader :state
66
+
67
+ # @return [CogboxApiClient::SandboxDesiredState] The desired state of the sandbox
68
+ attr_reader :desired_state
69
+
70
+ # @return [String] The error reason of the sandbox
71
+ attr_reader :error_reason
72
+
73
+ # @return [String] The state of the backup
74
+ attr_reader :backup_state
75
+
76
+ # @return [String, nil] The creation timestamp of the last backup.
77
+ # Not returned by list results; call #refresh on each item to populate.
78
+ attr_reader :backup_created_at
79
+
80
+ # @return [Float] Auto-stop interval in minutes (0 means disabled)
81
+ attr_reader :auto_stop_interval
82
+
83
+ # @return [Float] Auto-archive interval in minutes
84
+ attr_reader :auto_archive_interval
85
+
86
+ # @return [Float] Auto-delete interval in minutes
87
+ # (negative value means disabled, 0 means delete immediately upon stopping)
88
+ attr_reader :auto_delete_interval
89
+
90
+ # @return [Array<CogboxApiClient::SandboxVolume>, nil] Volumes attached to the sandbox.
91
+ # Not returned by list results; call #refresh on each item to populate.
92
+ attr_reader :volumes
93
+
94
+ # @return [CogboxApiClient::BuildInfo, nil] Build information for the sandbox if it was
95
+ # created from a dynamic build.
96
+ # Not returned by list results; call #refresh on each item to populate.
97
+ attr_reader :build_info
98
+
99
+ # @return [String] The creation timestamp of the sandbox
100
+ attr_reader :created_at
101
+
102
+ # @return [String] The last update timestamp of the sandbox
103
+ attr_reader :updated_at
104
+
105
+ # @return [String] The last activity timestamp of the sandbox
106
+ attr_reader :last_activity_at
107
+
108
+ # @return [String] The version of the daemon running in the sandbox
109
+ attr_reader :daemon_version
110
+
111
+ # @return [Cogbox::Config]
112
+ attr_reader :config
113
+
114
+ # @return [CogboxApiClient::SandboxApi]
115
+ attr_reader :sandbox_api
116
+
117
+ # @return [Cogbox::Process]
118
+ attr_reader :process
119
+
120
+ # @return [Cogbox::FileSystem]
121
+ attr_reader :fs
122
+
123
+ # @return [Cogbox::Git]
124
+ attr_reader :git
125
+
126
+ # @return [Cogbox::ComputerUse]
127
+ attr_reader :computer_use
128
+
129
+ # @return [Cogbox::CodeInterpreter]
130
+ attr_reader :code_interpreter
131
+
132
+ # @params config [Cogbox::Config]
133
+ # @params sandbox_api [CogboxApiClient::SandboxApi]
134
+ # @params sandbox_dto [CogboxApiClient::Sandbox, CogboxApiClient::SandboxListItem]
135
+ # @params otel_state [Cogbox::OtelState, nil]
136
+ def initialize(sandbox_dto:, config:, sandbox_api:, otel_state: nil) # rubocop:disable Metrics/MethodLength
137
+ process_response(sandbox_dto)
138
+ @config = config
139
+ @sandbox_api = sandbox_api
140
+ @otel_state = otel_state
141
+
142
+ # Create toolbox API clients with dynamic configuration
143
+ toolbox_api_config = build_toolbox_api_config
144
+
145
+ # Helper to create API client with authentication header
146
+ create_authenticated_client = lambda do
147
+ client = CogboxToolboxApiClient::ApiClient.new(toolbox_api_config)
148
+ client.default_headers['Authorization'] = "Bearer #{config.api_key || config.jwt_token}"
149
+ client.default_headers['X-Cogbox-Source'] = 'sdk-ruby'
150
+ client.default_headers['X-Cogbox-SDK-Version'] = Sdk::VERSION
151
+ client.default_headers['X-Cogbox-Organization-ID'] = config.organization_id if config.jwt_token
152
+ client.user_agent = "sdk-ruby/#{Sdk::VERSION}"
153
+ client
154
+ end
155
+
156
+ process_api = CogboxToolboxApiClient::ProcessApi.new(create_authenticated_client.call)
157
+ fs_api = CogboxToolboxApiClient::FileSystemApi.new(create_authenticated_client.call)
158
+ git_api = CogboxToolboxApiClient::GitApi.new(create_authenticated_client.call)
159
+ lsp_api = CogboxToolboxApiClient::LspApi.new(create_authenticated_client.call)
160
+ computer_use_api = CogboxToolboxApiClient::ComputerUseApi.new(create_authenticated_client.call)
161
+ interpreter_api = CogboxToolboxApiClient::InterpreterApi.new(create_authenticated_client.call)
162
+ info_api = CogboxToolboxApiClient::InfoApi.new(create_authenticated_client.call)
163
+
164
+ @process = Process.new(
165
+ sandbox_id: id,
166
+ toolbox_api: process_api,
167
+ get_preview_link: proc { |port| preview_url(port) },
168
+ language: (labels || {}).fetch(CODE_TOOLBOX_LANGUAGE_LABEL, 'python'),
169
+ otel_state:
170
+ )
171
+ @fs = FileSystem.new(sandbox_id: id, toolbox_api: fs_api, otel_state:)
172
+ @git = Git.new(sandbox_id: id, toolbox_api: git_api, otel_state:)
173
+ @computer_use = ComputerUse.new(sandbox_id: id, toolbox_api: computer_use_api, otel_state:)
174
+ @code_interpreter = CodeInterpreter.new(
175
+ sandbox_id: id,
176
+ toolbox_api: interpreter_api,
177
+ get_preview_link: proc { |port| preview_url(port) },
178
+ otel_state:
179
+ )
180
+ @lsp_api = lsp_api
181
+ @info_api = info_api
182
+ end
183
+
184
+ # Archives the sandbox, making it inactive and preserving its state. When sandboxes are
185
+ # archived, the entire filesystem state is moved to cost-effective object storage, making it
186
+ # possible to keep sandboxes available for an extended period. The tradeoff between archived
187
+ # and stopped states is that starting an archived sandbox takes more time, depending on its size.
188
+ # Sandbox must be stopped before archiving.
189
+ #
190
+ # @return [void]
191
+ def archive
192
+ sandbox_api.archive_sandbox(id)
193
+ refresh
194
+ end
195
+
196
+ # Sets the auto-archive interval for the Sandbox.
197
+ # The Sandbox will automatically archive after being continuously stopped for the specified interval.
198
+ #
199
+ # @param interval [Integer]
200
+ # @return [Integer]
201
+ # @raise [Cogbox:Sdk::Error]
202
+ def auto_archive_interval=(interval)
203
+ raise Sdk::Error, 'Auto-archive interval must be a non-negative integer' if interval.negative?
204
+
205
+ sandbox_api.set_auto_archive_interval(id, interval)
206
+ @auto_archive_interval = interval
207
+ end
208
+
209
+ # Sets the auto-delete interval for the Sandbox.
210
+ # The Sandbox will automatically delete after being continuously stopped for the specified interval.
211
+ #
212
+ # @param interval [Integer]
213
+ # @return [Integer]
214
+ # @raise [Cogbox:Sdk::Error]
215
+ def auto_delete_interval=(interval)
216
+ sandbox_api.set_auto_delete_interval(id, interval)
217
+ @auto_delete_interval = interval
218
+ end
219
+
220
+ # Updates outbound network policy on the runner (block all, restore access, or CIDR allow list).
221
+ #
222
+ # @param network_block_all [Boolean, nil]
223
+ # @param network_allow_list [String, nil]
224
+ # @param domain_allow_list [String, nil]
225
+ # @return [void]
226
+ # @raise [Cogbox::Sdk::Error]
227
+ def update_network_settings(network_block_all: nil, network_allow_list: nil, domain_allow_list: nil)
228
+ if network_block_all.nil? && network_allow_list.nil? && domain_allow_list.nil?
229
+ raise Sdk::Error,
230
+ 'At least one of network_block_all, network_allow_list or domain_allow_list must be provided'
231
+ end
232
+
233
+ body = CogboxApiClient::UpdateSandboxNetworkSettings.new(
234
+ network_block_all:,
235
+ network_allow_list:,
236
+ domain_allow_list:
237
+ )
238
+ data = sandbox_api.update_network_settings(id, body)
239
+ @network_block_all = data.network_block_all
240
+ @network_allow_list = data.network_allow_list
241
+ @domain_allow_list = data.domain_allow_list
242
+ end
243
+
244
+ # Sets the auto-stop interval for the Sandbox.
245
+ # The Sandbox will automatically stop after being idle (no new events) for the specified interval.
246
+ # Events include any state changes or interactions with the Sandbox through the SDK.
247
+ # Interactions using Sandbox Previews are not included.
248
+ #
249
+ # @param interval [Integer]
250
+ # @return [Integer]
251
+ # @raise [Cogbox:Sdk::Error]
252
+ def auto_stop_interval=(interval)
253
+ raise Sdk::Error, 'Auto-stop interval must be a non-negative integer' if interval.negative?
254
+
255
+ sandbox_api.set_autostop_interval(id, interval)
256
+ @auto_stop_interval = interval
257
+ end
258
+
259
+ # Creates an SSH access token for the sandbox.
260
+ #
261
+ # @param expires_in_minutes [Integer] TThe number of minutes the SSH access token will be valid for
262
+ # @return [CogboxApiClient::SshAccessDto]
263
+ def create_ssh_access(expires_in_minutes) = sandbox_api.create_ssh_access(id, { expires_in_minutes: })
264
+
265
+ # @return [void]
266
+ def delete
267
+ sandbox_api.delete_sandbox(id)
268
+ refresh
269
+ rescue CogboxApiClient::ApiError => e
270
+ raise unless e.code == 404
271
+
272
+ @state = 'destroyed'
273
+ end
274
+
275
+ # Gets the user's home directory path for the logged in user inside the Sandbox.
276
+ #
277
+ # @return [String] The absolute path to the Sandbox user's home directory for the logged in user
278
+ #
279
+ # @example
280
+ # user_home_dir = sandbox.get_user_home_dir
281
+ # puts "Sandbox user home: #{user_home_dir}"
282
+ def get_user_home_dir
283
+ @info_api.get_user_home_dir.dir
284
+ rescue StandardError => e
285
+ raise Sdk::Error, "Failed to get user home directory: #{e.message}"
286
+ end
287
+
288
+ # Gets the working directory path inside the Sandbox.
289
+ #
290
+ # @return [String] The absolute path to the Sandbox working directory. Uses the WORKDIR specified
291
+ # in the Dockerfile if present, or falling back to the user's home directory if not.
292
+ #
293
+ # @example
294
+ # work_dir = sandbox.get_work_dir
295
+ # puts "Sandbox working directory: #{work_dir}"
296
+ def get_work_dir
297
+ @info_api.get_work_dir.dir
298
+ rescue StandardError => e
299
+ raise Sdk::Error, "Failed to get working directory path: #{e.message}"
300
+ end
301
+
302
+ # Sets labels for the Sandbox.
303
+ #
304
+ # @param labels [Hash<String, String>]
305
+ # @return [Hash<String, String>]
306
+ def labels=(labels)
307
+ @labels = sandbox_api.replace_labels(id, CogboxApiClient::SandboxLabels.build_from_hash(labels:)).labels
308
+ end
309
+
310
+ # Retrieves the preview link for the sandbox at the specified port. If the port is closed,
311
+ # it will be opened automatically. For private sandboxes, a token is included to grant access
312
+ # to the URL.
313
+ #
314
+ # @param port [Integer]
315
+ # @return [CogboxApiClient::PortPreviewUrl]
316
+ def preview_url(port) = sandbox_api.get_port_preview_url(id, port)
317
+
318
+ # Creates a signed preview URL for the sandbox at the specified port.
319
+ #
320
+ # @param port [Integer] The port to open the preview link on
321
+ # @param expires_in_seconds [Integer, nil] The number of seconds the signed preview URL
322
+ # will be valid for. Defaults to 60 seconds.
323
+ # @return [CogboxApiClient::SignedPortPreviewUrl] The signed preview URL response object
324
+ #
325
+ # @example
326
+ # signed_url = sandbox.create_signed_preview_url(3000, 120)
327
+ # puts "Signed URL: #{signed_url.url}"
328
+ # puts "Token: #{signed_url.token}"
329
+ def create_signed_preview_url(port, expires_in_seconds = nil)
330
+ sandbox_api.get_signed_port_preview_url(id, port, { expires_in_seconds: })
331
+ end
332
+
333
+ # Expires a signed preview URL for the sandbox at the specified port.
334
+ #
335
+ # @param port [Integer] The port to expire the signed preview URL on
336
+ # @param token [String] The token to expire
337
+ # @return [void]
338
+ #
339
+ # @example
340
+ # sandbox.expire_signed_preview_url(3000, "token-value")
341
+ def expire_signed_preview_url(port, token)
342
+ sandbox_api.expire_signed_port_preview_url(id, port, token)
343
+ end
344
+
345
+ # Refresh the Sandbox data from the API.
346
+ #
347
+ # @return [void]
348
+ def refresh = process_response(sandbox_api.get_sandbox(id))
349
+
350
+ # Refreshes the sandbox activity to reset the timer for automated lifecycle management actions.
351
+ #
352
+ # This method updates the sandbox's last activity timestamp without changing its state.
353
+ # It is useful for keeping long-running sessions alive while there is still user activity.
354
+ #
355
+ # @return [void]
356
+ #
357
+ # @example
358
+ # sandbox.refresh_activity
359
+ def refresh_activity
360
+ sandbox_api.update_last_activity(id)
361
+ nil
362
+ rescue StandardError => e
363
+ raise Sdk::Error, "Failed to refresh sandbox activity: #{e.message}"
364
+ end
365
+
366
+ # Revokes an SSH access token for the sandbox.
367
+ #
368
+ # @param token [String]
369
+ # @return [void]
370
+ def revoke_ssh_access(token) = sandbox_api.revoke_ssh_access(id, token:)
371
+
372
+ # Starts the Sandbox and waits for it to be ready.
373
+ #
374
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
375
+ # @return [void]
376
+ def start(timeout = DEFAULT_TIMEOUT)
377
+ with_timeout(
378
+ timeout:,
379
+ message: "Sandbox #{id} failed to become ready within the #{timeout} seconds timeout period",
380
+ setup: proc { process_response(sandbox_api.start_sandbox(id)) }
381
+ ) { wait_for_states(operation: OPERATION_START, target_states: [CogboxApiClient::SandboxState::STARTED]) }
382
+ end
383
+
384
+ # Recovers the Sandbox from a recoverable error and waits for it to be ready.
385
+ #
386
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
387
+ # @return [void]
388
+ #
389
+ # @example
390
+ # sandbox = cogbox.get('my-sandbox-id')
391
+ # sandbox.recover(timeout: 40) # Wait up to 40 seconds
392
+ # puts 'Sandbox recovered successfully'
393
+ def recover(timeout = DEFAULT_TIMEOUT)
394
+ with_timeout(
395
+ timeout:,
396
+ message: "Sandbox #{id} failed to recover within the #{timeout} seconds timeout period",
397
+ setup: proc { process_response(sandbox_api.recover_sandbox(id)) }
398
+ ) { wait_for_states(operation: OPERATION_START, target_states: [CogboxApiClient::SandboxState::STARTED]) }
399
+ rescue StandardError => e
400
+ raise Sdk::Error, "Failed to recover sandbox: #{e.message}"
401
+ end
402
+
403
+ # Stops the Sandbox and waits for it to be stopped.
404
+ #
405
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
406
+ # @param force [Boolean] If true, uses SIGKILL instead of SIGTERM (defaults to false).
407
+ # @return [void]
408
+ def stop(timeout = DEFAULT_TIMEOUT, force: false) # rubocop:disable Metrics/MethodLength
409
+ with_timeout(
410
+ timeout:,
411
+ message: "Sandbox #{id} failed to become stopped within the #{timeout} seconds timeout period",
412
+ setup: proc {
413
+ sandbox_api.stop_sandbox(id, { force: force })
414
+ refresh
415
+ }
416
+ ) do
417
+ wait_for_states(
418
+ operation: OPERATION_STOP,
419
+ target_states: [CogboxApiClient::SandboxState::STOPPED, CogboxApiClient::SandboxState::DESTROYED]
420
+ )
421
+ end
422
+ end
423
+
424
+ # Resizes the Sandbox resources.
425
+ #
426
+ # Changes the CPU, memory, or disk allocation. Resizing a started sandbox accepts
427
+ # only CPU and memory increases. Disk resize requires a stopped sandbox; disk can
428
+ # only grow. GPU is not resizable — to change GPU, create a new sandbox.
429
+ #
430
+ # @param resources [Cogbox::Resources] New resource configuration (cpu, memory, disk only)
431
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
432
+ # @return [void]
433
+ # @raise [Sdk::Error] If resources.gpu or resources.gpu_type is set
434
+ #
435
+ # @example Resize a started sandbox (CPU and memory can be increased)
436
+ # sandbox.resize(Cogbox::Resources.new(cpu: 4, memory: 8))
437
+ #
438
+ # @example Resize a stopped sandbox (CPU, memory, and disk can be changed)
439
+ # sandbox.stop
440
+ # sandbox.resize(Cogbox::Resources.new(cpu: 2, memory: 4, disk: 30))
441
+ def resize(resources, timeout = DEFAULT_TIMEOUT)
442
+ raise Sdk::Error, 'Resources must not be nil' if resources.nil?
443
+
444
+ if resources.gpu || resources.gpu_type
445
+ raise Sdk::Error,
446
+ 'Resize does not support changes to gpu or gpu_type — to change GPU, create a new sandbox'
447
+ end
448
+
449
+ with_timeout(
450
+ timeout:,
451
+ message: "Sandbox #{id} failed to resize within the #{timeout} seconds timeout period",
452
+ setup: proc {
453
+ resize_attrs = {}
454
+ resize_attrs[:cpu] = resources.cpu if resources.cpu
455
+ resize_attrs[:memory] = resources.memory if resources.memory
456
+ resize_attrs[:disk] = resources.disk if resources.disk
457
+ resize_request = CogboxApiClient::ResizeSandbox.new(resize_attrs)
458
+ process_response(sandbox_api.resize_sandbox(id, resize_request))
459
+ }
460
+ ) { wait_for_resize_complete }
461
+ end
462
+
463
+ # Waits for the Sandbox resize operation to complete.
464
+ # Polls the Sandbox status until the state is no longer 'resizing'.
465
+ #
466
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
467
+ # @return [void]
468
+ def wait_for_resize_complete(_timeout = DEFAULT_TIMEOUT)
469
+ wait_for_states(operation: OPERATION_RESIZE, target_states: [CogboxApiClient::SandboxState::STARTED,
470
+ CogboxApiClient::SandboxState::STOPPED])
471
+ end
472
+
473
+ # Creates a new Language Server Protocol (LSP) server instance.
474
+ # The LSP server provides language-specific features like code completion,
475
+ # diagnostics, and more.
476
+ #
477
+ # @param language_id [Symbol] The language server type (e.g., Cogbox::LspServer::Language::PYTHON)
478
+ # @param path_to_project [String] Path to the project root directory. Relative paths are resolved
479
+ # based on the sandbox working directory.
480
+ # @return [Cogbox::LspServer]
481
+ def create_lsp_server(language_id:, path_to_project:)
482
+ LspServer.new(language_id:, path_to_project:, toolbox_api: @lsp_api, sandbox_id: id, otel_state:)
483
+ end
484
+
485
+ # Validates an SSH access token for the sandbox.
486
+ #
487
+ # @param token [String]
488
+ # @return [CogboxApiClient::SshAccessValidationDto]
489
+ def validate_ssh_access(token) = sandbox_api.validate_ssh_access(token)
490
+
491
+ # Waits for the Sandbox to reach the 'started' state. Polls the Sandbox status until it
492
+ # reaches the 'started' state or encounters an error.
493
+ #
494
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
495
+ # @return [void]
496
+ def wait_for_sandbox_start(_timeout = DEFAULT_TIMEOUT)
497
+ wait_for_states(operation: OPERATION_START, target_states: [CogboxApiClient::SandboxState::STARTED])
498
+ end
499
+
500
+ # Waits for the Sandbox to reach the 'stopped' state. Polls the Sandbox status until it
501
+ # reaches the 'stopped' state or encounters an error.
502
+ # Treats destroyed as stopped to cover ephemeral sandboxes that are automatically deleted after stopping.
503
+ #
504
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
505
+ # @return [void]
506
+ def wait_for_sandbox_stop(_timeout = DEFAULT_TIMEOUT)
507
+ wait_for_states(operation: OPERATION_STOP, target_states: [CogboxApiClient::SandboxState::STOPPED,
508
+ CogboxApiClient::SandboxState::DESTROYED])
509
+ end
510
+
511
+ # Forks the Sandbox, creating a new Sandbox with an identical filesystem.
512
+ # The forked Sandbox is a copy-on-write clone of the original. It starts
513
+ # with the same disk contents but operates independently from that point on.
514
+ #
515
+ # @param name [String, nil] Optional name for the forked Sandbox
516
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
517
+ # @return [Cogbox::Sandbox] The forked Sandbox
518
+ def experimental_fork(name: nil, timeout: DEFAULT_TIMEOUT) # rubocop:disable Metrics/MethodLength
519
+ forked_dto = nil
520
+ with_timeout(
521
+ timeout:,
522
+ message: "Sandbox #{id} fork failed to become ready within the #{timeout} seconds timeout period",
523
+ setup: proc {
524
+ forked_dto = sandbox_api.fork_sandbox(id, CogboxApiClient::ForkSandbox.new(name:))
525
+ }
526
+ ) do
527
+ forked = Sandbox.new(
528
+ sandbox_dto: forked_dto,
529
+ config:,
530
+ sandbox_api:,
531
+ code_toolbox:,
532
+ otel_state:
533
+ )
534
+ forked.send(:wait_for_states, operation: OPERATION_START,
535
+ target_states: [CogboxApiClient::SandboxState::STARTED])
536
+ return forked
537
+ end
538
+ end
539
+
540
+ # Creates a snapshot from the current state of the Sandbox.
541
+ # The Sandbox will temporarily enter a 'snapshotting' state and return to its previous state when complete.
542
+ #
543
+ # @param name [String] Name for the new snapshot
544
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
545
+ # @return [void]
546
+ def experimental_create_snapshot(name:, timeout: DEFAULT_TIMEOUT)
547
+ with_timeout(
548
+ timeout:,
549
+ message: "Sandbox #{id} snapshot failed within the #{timeout} seconds timeout period",
550
+ setup: proc {
551
+ sandbox_api.create_sandbox_snapshot(id, CogboxApiClient::CreateSandboxSnapshot.new(name:))
552
+ refresh
553
+ }
554
+ ) { wait_for_snapshot_complete }
555
+ end
556
+
557
+ # Pauses the Sandbox, freezing all running processes.
558
+ # The Sandbox will enter a 'pausing' state and transition to 'paused' when complete.
559
+ #
560
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
561
+ # @return [void]
562
+ def pause(timeout: DEFAULT_TIMEOUT)
563
+ with_timeout(
564
+ timeout:,
565
+ message: "Sandbox #{id} failed to pause within the #{timeout} seconds timeout period",
566
+ setup: proc {
567
+ sandbox_api.pause_sandbox(id)
568
+ refresh
569
+ }
570
+ ) { wait_for_pause_complete }
571
+ end
572
+
573
+ instrument :archive, :auto_archive_interval=, :auto_delete_interval=, :auto_stop_interval=,
574
+ :update_network_settings,
575
+ :create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :labels=,
576
+ :preview_url, :create_signed_preview_url, :expire_signed_preview_url,
577
+ :refresh, :refresh_activity, :revoke_ssh_access, :start, :recover, :stop,
578
+ :create_lsp_server, :validate_ssh_access, :wait_for_sandbox_start,
579
+ :wait_for_sandbox_stop, :resize, :wait_for_resize_complete,
580
+ :experimental_fork, :experimental_create_snapshot, :pause,
581
+ component: 'Sandbox'
582
+
583
+ private
584
+
585
+ # @return [Cogbox::OtelState, nil]
586
+ attr_reader :otel_state
587
+
588
+ # Build toolbox API configuration with dynamic base URL from preview link
589
+ # @return [CogboxToolboxApiClient::Configuration]
590
+ def build_toolbox_api_config
591
+ CogboxToolboxApiClient::Configuration.new.configure do |cfg|
592
+ proxy_url = @toolbox_proxy_url
593
+ proxy_url += '/' unless proxy_url.end_with?('/')
594
+ full_url = "#{proxy_url}#{id}"
595
+ uri = URI(full_url)
596
+
597
+ cfg.scheme = uri.scheme
598
+ cfg.host = uri.authority # Includes hostname:port
599
+ cfg.base_path = uri.path.empty? ? '/' : uri.path
600
+
601
+ cfg
602
+ end
603
+ end
604
+
605
+ # @params sandbox_dto [CogboxApiClient::Sandbox, CogboxApiClient::SandboxListItem]
606
+ # @return [void]
607
+ def process_response(sandbox_dto) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
608
+ # Fields shared by both CogboxApiClient::Sandbox and CogboxApiClient::SandboxListItem.
609
+ @id = sandbox_dto.id
610
+ @organization_id = sandbox_dto.organization_id
611
+ @snapshot = sandbox_dto.snapshot
612
+ @user = sandbox_dto.user
613
+ @labels = sandbox_dto.labels
614
+ @public = sandbox_dto.public
615
+ @target = sandbox_dto.target
616
+ @cpu = sandbox_dto.cpu
617
+ @gpu = sandbox_dto.gpu
618
+ @memory = sandbox_dto.memory
619
+ @disk = sandbox_dto.disk
620
+ @state = sandbox_dto.state
621
+ @desired_state = sandbox_dto.desired_state
622
+ @error_reason = sandbox_dto.error_reason
623
+ @backup_state = sandbox_dto.backup_state
624
+ @auto_stop_interval = sandbox_dto.auto_stop_interval
625
+ @auto_archive_interval = sandbox_dto.auto_archive_interval
626
+ @auto_delete_interval = sandbox_dto.auto_delete_interval
627
+ @created_at = sandbox_dto.created_at
628
+ @updated_at = sandbox_dto.updated_at
629
+ @last_activity_at = sandbox_dto.last_activity_at
630
+ @daemon_version = sandbox_dto.daemon_version
631
+ @toolbox_proxy_url = sandbox_dto.toolbox_proxy_url
632
+
633
+ # Fields only present on the full CogboxApiClient::Sandbox DTO (not returned by list
634
+ # results; call #refresh on each item to populate them).
635
+ return unless sandbox_dto.is_a?(CogboxApiClient::Sandbox)
636
+
637
+ @env = sandbox_dto.env
638
+ @network_block_all = sandbox_dto.network_block_all
639
+ @network_allow_list = sandbox_dto.network_allow_list
640
+ @domain_allow_list = sandbox_dto.domain_allow_list
641
+ @volumes = sandbox_dto.volumes
642
+ @build_info = sandbox_dto.build_info
643
+ @backup_created_at = sandbox_dto.backup_created_at
644
+ end
645
+
646
+ # Monitors block not to exceed max execution time.
647
+ #
648
+ # @param setup [#call, Nil] Optional setup block
649
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
650
+ # @param message [String] Error message
651
+ # @return [void]
652
+ # @raise [Cogbox::Sdk::Error]
653
+ def with_timeout(message:, setup:, timeout: DEFAULT_TIMEOUT, &)
654
+ start_at = Time.now
655
+ setup&.call
656
+
657
+ Timeout.timeout(
658
+ setup ? [NO_TIMEOUT, timeout - (Time.now - start_at)].max : timeout,
659
+ Sdk::Error,
660
+ message,
661
+ &
662
+ )
663
+ end
664
+
665
+ # Waits for the Sandbox to reach the one of the target states. Polls the Sandbox status until it
666
+ # reaches the one of the target states or encounters an error. It will wait up to 60 seconds
667
+ # for the Sandbox to reach one of the target states.
668
+ #
669
+ # @param operation [#to_s] Operation name for error message
670
+ # @param target_states [Array<CogboxApiClient::SandboxState>] List of the target states
671
+ # @return [void]
672
+ # @raise [Cogbox::Sdk::Error]
673
+ def wait_for_states(operation:, target_states:)
674
+ interval = INITIAL_POLL_INTERVAL
675
+ start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
676
+ loop do
677
+ case state
678
+ when *target_states then return
679
+ when CogboxApiClient::SandboxState::ERROR, CogboxApiClient::SandboxState::BUILD_FAILED
680
+ raise Sdk::Error, "Sandbox #{id} failed to #{operation} with state: #{state}, error reason: #{error_reason}"
681
+ end
682
+
683
+ sleep(interval)
684
+ if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time > 5
685
+ interval = [interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min
686
+ end
687
+ refresh
688
+ end
689
+ end
690
+
691
+ INITIAL_POLL_INTERVAL = 0.1
692
+ private_constant :INITIAL_POLL_INTERVAL
693
+
694
+ MAX_POLL_INTERVAL = 1.0
695
+ private_constant :MAX_POLL_INTERVAL
696
+
697
+ BACKOFF_MULTIPLIER = 1.1
698
+ private_constant :BACKOFF_MULTIPLIER
699
+
700
+ NO_TIMEOUT = 0
701
+ private_constant :NO_TIMEOUT
702
+
703
+ OPERATION_START = :start
704
+ private_constant :OPERATION_START
705
+
706
+ OPERATION_STOP = :stop
707
+ private_constant :OPERATION_STOP
708
+
709
+ OPERATION_RESIZE = :resize
710
+ private_constant :OPERATION_RESIZE
711
+
712
+ def wait_for_snapshot_complete
713
+ interval = INITIAL_POLL_INTERVAL
714
+ start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
715
+ while state == CogboxApiClient::SandboxState::SNAPSHOTTING
716
+ refresh
717
+
718
+ if [CogboxApiClient::SandboxState::ERROR, CogboxApiClient::SandboxState::BUILD_FAILED].include?(state)
719
+ raise Sdk::Error,
720
+ "Sandbox #{id} snapshot failed with state: #{state}, error reason: #{error_reason}"
721
+ end
722
+
723
+ break if state != CogboxApiClient::SandboxState::SNAPSHOTTING
724
+
725
+ sleep(interval)
726
+ if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time > 5
727
+ interval = [interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min
728
+ end
729
+ end
730
+ end
731
+
732
+ def wait_for_pause_complete
733
+ interval = INITIAL_POLL_INTERVAL
734
+ start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
735
+ while state == CogboxApiClient::SandboxState::PAUSING
736
+ refresh
737
+
738
+ if [CogboxApiClient::SandboxState::ERROR, CogboxApiClient::SandboxState::BUILD_FAILED].include?(state)
739
+ raise Sdk::Error,
740
+ "Sandbox #{id} pause failed with state: #{state}, error reason: #{error_reason}"
741
+ end
742
+
743
+ break if state != CogboxApiClient::SandboxState::PAUSING
744
+
745
+ sleep(interval)
746
+ if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time > 5
747
+ interval = [interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min
748
+ end
749
+ end
750
+ end
751
+ end
752
+ end