daytona 0.184.0 → 0.185.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a915942565b3350f301af5bc17108ed23fd4c17ab2fabf794c55d061f7001b05
4
- data.tar.gz: b7c4ae7fb09fd196a84b5b63fb06e66300ca16ef30d18f5b87c8026e0aa0366c
3
+ metadata.gz: '0846ab327fc5ae0c55ee61872fb3c29bf266979387b8f362846d0856dee50b0d'
4
+ data.tar.gz: 6ea941ba9e0c6182cbd9c364174ff58ec32eba00fa9c252d2decb5131bd19676
5
5
  SHA512:
6
- metadata.gz: a849bfb163dbd2ec05b7fbc152877d702195526bf28c1c74e3da19b79d454d338deed08f4bc503998594ba65549f1b7960af64383bc6da67402751faebd09d45
7
- data.tar.gz: 7769c3074cda40aad265165e41755e6e6de16144a36b1bbc1e739f8b3e5b9c20d0d57c4eec20dccaa97322b412068c663f3d86f2ecf1514c3cb18aceb39337c7
6
+ metadata.gz: 8f7da0103eee9ba48d742003281d86530482aa1504e4c8d60734841c2c1229b47270c1d646b17cd70071e0fb314feb3c3fa262c92348324dd2015a062d67a853
7
+ data.tar.gz: 801300d22b0ebc53f76d870a4f98eca73b0bf267b8b877d33f6be41cf3c6c35790971d9abbec4e5241f4335ef7c0aa44b2e9958ea8fe7579d562e778fb8a49de
@@ -108,40 +108,40 @@ module Daytona
108
108
 
109
109
  puts "[DEBUG] Connecting to WebSocket: #{ws_url}" if ENV['DEBUG']
110
110
 
111
- ws = WebSocket::Client::Simple.connect(ws_url, headers:)
112
-
113
- ws.on :open do
114
- puts '[DEBUG] WebSocket opened, sending request' if ENV['DEBUG']
115
- ws.send(JSON.dump(request))
116
- end
117
-
118
- ws.on :message do |msg|
119
- puts "[DEBUG] Received message (length=#{msg.data.length}): #{msg.data.inspect[0..200]}" if ENV['DEBUG']
111
+ ws = WebSocket::Client::Simple.connect(ws_url, headers:) do |client|
112
+ client.on :open do
113
+ puts '[DEBUG] WebSocket opened, sending request' if ENV['DEBUG']
114
+ client.send(JSON.dump(request))
115
+ end
120
116
 
121
- interpreter.send(:handle_message, msg.data, result, on_stdout, on_stderr, on_error, completion_queue)
122
- end
117
+ client.on :message do |msg|
118
+ puts "[DEBUG] Received message (length=#{msg.data.length}): #{msg.data.inspect[0..200]}" if ENV['DEBUG']
123
119
 
124
- ws.on :error do |e|
125
- puts "[DEBUG] WebSocket error: #{e.message}" if ENV['DEBUG']
126
- completion_queue.push({ type: :error, error: e })
127
- end
120
+ interpreter.send(:handle_message, msg.data, result, on_stdout, on_stderr, on_error, completion_queue)
121
+ end
128
122
 
129
- ws.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}"
123
+ client.on :error do |e|
124
+ puts "[DEBUG] WebSocket error: #{e.message}" if ENV['DEBUG']
125
+ completion_queue.push({ type: :error, error: e })
134
126
  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 })
127
+
128
+ client.on :close do |e|
129
+ if ENV['DEBUG']
130
+ code = e&.code || 'nil'
131
+ reason = e&.reason || 'nil'
132
+ puts "[DEBUG] WebSocket closed: code=#{code}, reason=#{reason}"
133
+ end
134
+ error_info = interpreter.send(:handle_close, e)
135
+ if error_info
136
+ completion_queue.push({ type: :error_from_close, error: error_info })
137
+ else
138
+ completion_queue.push({ type: :close })
139
+ end
140
140
  end
141
141
  end
142
142
 
143
143
  no_timeout = timeout.is_a?(Numeric) && timeout <= 0
144
- max_wait = no_timeout ? nil : (timeout || 600) + 3
144
+ max_wait = no_timeout ? nil : (timeout || 600) + 30
145
145
  start_time = Time.now
146
146
  completion_reason = nil
147
147
 
@@ -6,6 +6,7 @@
6
6
  module Daytona
7
7
  # Re-export of api-client enum constants under the Daytona namespace so
8
8
  # SDK consumers never need to import from DaytonaApiClient directly.
9
+ SandboxClass = DaytonaApiClient::SandboxClass
9
10
  SandboxState = DaytonaApiClient::SandboxState
10
11
  SandboxListSortField = DaytonaApiClient::SandboxListSortField
11
12
  SandboxListSortDirection = DaytonaApiClient::SandboxListSortDirection
@@ -23,17 +23,22 @@ module Daytona
23
23
  # Defaults to organization default region if not specified.
24
24
  attr_reader :region_id
25
25
 
26
+ # @return [DaytonaApiClient::SandboxClass, nil] Target sandbox class.
27
+ attr_reader :sandbox_class
28
+
26
29
  # @param name [String] Name of the snapshot
27
30
  # @param image [String, Daytona::Image] Image of the snapshot
28
31
  # @param resources [Daytona::Resources, nil] Resources of the snapshot
29
32
  # @param entrypoint [Array<String>, nil] Entrypoint of the snapshot
30
33
  # @param region_id [String, nil] ID of the region where the snapshot will be available
31
- def initialize(name:, image:, resources: nil, entrypoint: nil, region_id: nil)
34
+ # @param sandbox_class [DaytonaApiClient::SandboxClass, nil] Target sandbox class
35
+ def initialize(name:, image:, resources: nil, entrypoint: nil, region_id: nil, sandbox_class: nil)
32
36
  @name = name
33
37
  @image = image
34
38
  @resources = resources
35
39
  @entrypoint = entrypoint
36
40
  @region_id = region_id
41
+ @sandbox_class = sandbox_class
37
42
  end
38
43
  end
39
44
 
@@ -213,7 +213,7 @@ module Daytona
213
213
  # # Upload binary data
214
214
  # data = { key: "value" }.to_json
215
215
  # sandbox.fs.upload_file(data, "tmp/config.json")
216
- def upload_file(source, remote_path) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
216
+ def upload_file(source, remote_path)
217
217
  if source.is_a?(String) && File.exist?(source)
218
218
  # Source is a file path
219
219
  File.open(source, 'rb') { |file| toolbox_api.upload_file(remote_path, file) }
@@ -221,12 +221,17 @@ module Daytona
221
221
  # Source is an IO object
222
222
  toolbox_api.upload_file(remote_path, source)
223
223
  else
224
- # Source is string content - create a temporary file
225
- Tempfile.create('daytona_upload') do |file|
226
- file.binmode
227
- file.write(source)
228
- file.rewind
229
- toolbox_api.upload_file(remote_path, file)
224
+ # Tempfile.create yields a ::File (works with Typhoeus) but deletes
225
+ # on block exit — too early if curl reads asynchronously. Write via
226
+ # Tempfile, close it, then reopen as ::File for the upload.
227
+ tmp = Tempfile.new('daytona_upload')
228
+ begin
229
+ tmp.binmode
230
+ tmp.write(source.to_s.b)
231
+ tmp.close
232
+ File.open(tmp.path, 'rb') { |file| toolbox_api.upload_file(remote_path, file) }
233
+ ensure
234
+ tmp.unlink
230
235
  end
231
236
  end
232
237
  rescue StandardError => e
@@ -18,6 +18,7 @@ module Daytona
18
18
  class MultipartDownloadStreamParser
19
19
  attr_reader :error_message
20
20
  attr_reader :part_total_bytes
21
+ attr_reader :part_bytes_emitted
21
22
  attr_writer :boundary_token
22
23
 
23
24
  def initialize(&on_file_chunk)
@@ -27,6 +28,7 @@ module Daytona
27
28
  @state = :preamble
28
29
  @part_name = nil
29
30
  @part_total_bytes = nil
31
+ @part_bytes_emitted = 0
30
32
  @error_buffer = String.new.b
31
33
  end
32
34
 
@@ -35,15 +37,13 @@ module Daytona
35
37
  process!
36
38
  end
37
39
 
40
+ # Raises if the response ended before the closing multipart boundary, so
41
+ # truncations surface as typed errors instead of silently short downloads.
38
42
  def finish!
39
43
  process!
44
+ return if @state == :done
40
45
 
41
- return if @state == :done || @buffer.empty?
42
-
43
- emit(@buffer)
44
- finalize_part!
45
- @buffer = String.new.b
46
- @state = :done
46
+ raise Sdk::Error, "Truncated multipart response: closing boundary not received (state=#{@state})"
47
47
  end
48
48
 
49
49
  private
@@ -110,6 +110,7 @@ module Daytona
110
110
 
111
111
  case @part_name
112
112
  when 'file'
113
+ @part_bytes_emitted += data.bytesize
113
114
  @on_file_chunk.call(data)
114
115
  when 'error'
115
116
  @error_buffer << data
@@ -201,7 +202,7 @@ module Daytona
201
202
 
202
203
  request.on_complete do |completed_response|
203
204
  response = completed_response
204
- parser.finish!
205
+ parser.finish! unless cancel_event&.set?
205
206
  end
206
207
 
207
208
  request.run
@@ -209,6 +210,16 @@ module Daytona
209
210
  raise Sdk::Error, "Download cancelled: #{remote_path}" if cancel_event&.set?
210
211
  raise Sdk::Error, parser.error_message if parser.error_message
211
212
  raise Sdk::Error, "HTTP #{response.code}" if response && !response.success?
213
+
214
+ assert_download_length!(parser, remote_path)
215
+ end
216
+
217
+ def self.assert_download_length!(parser, remote_path)
218
+ return unless parser.part_total_bytes && parser.part_bytes_emitted != parser.part_total_bytes
219
+
220
+ raise Sdk::Error,
221
+ "Multipart response length mismatch for #{remote_path}: " \
222
+ "got #{parser.part_bytes_emitted} bytes, expected #{parser.part_total_bytes}"
212
223
  end
213
224
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
214
225
 
@@ -232,6 +243,7 @@ module Daytona
232
243
  def self.stream_upload(api_client:, remote_path:, source:, timeout:, on_progress: nil, cancel_event: nil)
233
244
  with_upload_file(source, cancel_event, remote_path) do |upload_path|
234
245
  config = api_client.config
246
+ expected_bytes = File.size(upload_path)
235
247
  progress_callback = upload_progress_callback(on_progress, cancel_event)
236
248
  response = with_open_upload_file(upload_path) do |file|
237
249
  upload_request(
@@ -244,6 +256,7 @@ module Daytona
244
256
  ).run
245
257
  end
246
258
  raise_upload_error(response, cancel_event, remote_path)
259
+ verify_upload_response(response, remote_path, expected_bytes)
247
260
  end
248
261
  end
249
262
  # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
@@ -338,6 +351,28 @@ module Daytona
338
351
  raise Sdk::Error, "HTTP #{response.code}: #{response.body}" unless response.success?
339
352
  end
340
353
 
354
+ # Compares the daemon's reported bytes-written against what the SDK sent.
355
+ # Catches server-side miscounts (or extra-byte injection) at the upload
356
+ # call site instead of surfacing later as a download mismatch.
357
+ def self.verify_upload_response(response, remote_path, expected_bytes)
358
+ recorded = recorded_upload_bytes(response.body, remote_path)
359
+ return if recorded.nil? || recorded == expected_bytes
360
+
361
+ raise Sdk::Error,
362
+ "Upload size mismatch for #{remote_path}: sent #{expected_bytes} bytes, " \
363
+ "daemon recorded #{recorded}"
364
+ end
365
+
366
+ def self.recorded_upload_bytes(body, remote_path)
367
+ parsed = JSON.parse(body) rescue nil # rubocop:disable Style/RescueModifier
368
+ return nil unless parsed.is_a?(Hash)
369
+
370
+ files = Array(parsed['files'])
371
+ match = files.find { |f| f.is_a?(Hash) && f['path'] == remote_path }
372
+ bytes = match&.dig('bytes')
373
+ bytes.is_a?(Integer) ? bytes : nil
374
+ end
375
+
341
376
  def self.open_drain_source(source)
342
377
  return [source, false] if source.respond_to?(:read)
343
378
  return [StringIO.new(source.b), true] if source.is_a?(String)
data/lib/daytona/git.rb CHANGED
@@ -82,6 +82,9 @@ module Daytona
82
82
  # the repository will be left in a detached HEAD state at this commit.
83
83
  # @param username [String, nil] Git username for authentication.
84
84
  # @param password [String, nil] Git password or token for authentication.
85
+ # @param insecure_skip_tls [Boolean, nil] Skip TLS certificate verification (insecure).
86
+ # Use only for trusted internal Git servers with self-signed or private-CA certs;
87
+ # credentials, if supplied, are transmitted over an unverified TLS connection.
85
88
  # @return [void]
86
89
  # @raise [Daytona::Sdk::Error] if cloning repository fails
87
90
  #
@@ -107,7 +110,7 @@ module Daytona
107
110
  # path: "workspace/repo-old",
108
111
  # commit_id: "abc123"
109
112
  # )
110
- def clone(url:, path:, branch: nil, commit_id: nil, username: nil, password: nil) # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
113
+ def clone(url:, path:, branch: nil, commit_id: nil, username: nil, password: nil, insecure_skip_tls: nil) # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
111
114
  toolbox_api.clone_repository(
112
115
  DaytonaToolboxApiClient::GitCloneRequest.new(
113
116
  url: url,
@@ -115,7 +118,8 @@ module Daytona
115
118
  path: path,
116
119
  username: username,
117
120
  password: password,
118
- commit_id: commit_id
121
+ commit_id: commit_id,
122
+ insecure_skip_tls: insecure_skip_tls
119
123
  )
120
124
  )
121
125
  rescue DaytonaToolboxApiClient::ApiError => e
@@ -235,34 +235,34 @@ module Daytona
235
235
 
236
236
  completion_queue = Queue.new
237
237
 
238
- ws = WebSocket::Client::Simple.connect(
238
+ WebSocket::Client::Simple.connect(
239
239
  url.to_s,
240
240
  headers: toolbox_api.api_client.default_headers.dup.merge(
241
241
  'X-Daytona-Preview-Token' => preview_link.token,
242
242
  'Content-Type' => 'text/plain',
243
243
  'Accept' => 'text/plain'
244
244
  )
245
- )
245
+ ) do |client|
246
+ client.on(:message) do |message|
247
+ if message.type == :close
248
+ client.close
249
+ completion_queue.push(:close)
250
+ else
251
+ stdout, stderr = Util.demux(message.data.to_s)
252
+
253
+ on_stdout.call(stdout) unless stdout.empty?
254
+ on_stderr.call(stderr) unless stderr.empty?
255
+ end
256
+ end
246
257
 
247
- ws.on(:message) do |message|
248
- if message.type == :close
249
- ws.close
258
+ client.on(:close) do
250
259
  completion_queue.push(:close)
251
- else
252
- stdout, stderr = Util.demux(message.data.to_s)
253
-
254
- on_stdout.call(stdout) unless stdout.empty?
255
- on_stderr.call(stderr) unless stderr.empty?
256
260
  end
257
- end
258
-
259
- ws.on(:close) do
260
- completion_queue.push(:close)
261
- end
262
261
 
263
- ws.on(:error) do |e|
264
- completion_queue.push(:error)
265
- raise Sdk::Error, "WebSocket error: #{e.message}"
262
+ client.on(:error) do |e|
263
+ completion_queue.push(:error)
264
+ raise Sdk::Error, "WebSocket error: #{e.message}"
265
+ end
266
266
  end
267
267
 
268
268
  # Wait for completion
@@ -302,34 +302,34 @@ module Daytona
302
302
 
303
303
  completion_queue = Queue.new
304
304
 
305
- ws = WebSocket::Client::Simple.connect(
305
+ WebSocket::Client::Simple.connect(
306
306
  url.to_s,
307
307
  headers: toolbox_api.api_client.default_headers.dup.merge(
308
308
  'X-Daytona-Preview-Token' => preview_link.token,
309
309
  'Content-Type' => 'text/plain',
310
310
  'Accept' => 'text/plain'
311
311
  )
312
- )
312
+ ) do |client|
313
+ client.on(:message) do |message|
314
+ if message.type == :close
315
+ client.close
316
+ completion_queue.push(:close)
317
+ else
318
+ stdout, stderr = Util.demux(message.data.to_s)
319
+
320
+ on_stdout.call(stdout) unless stdout.empty?
321
+ on_stderr.call(stderr) unless stderr.empty?
322
+ end
323
+ end
313
324
 
314
- ws.on(:message) do |message|
315
- if message.type == :close
316
- ws.close
325
+ client.on(:close) do
317
326
  completion_queue.push(:close)
318
- else
319
- stdout, stderr = Util.demux(message.data.to_s)
320
-
321
- on_stdout.call(stdout) unless stdout.empty?
322
- on_stderr.call(stderr) unless stderr.empty?
323
327
  end
324
- end
325
-
326
- ws.on(:close) do
327
- completion_queue.push(:close)
328
- end
329
328
 
330
- ws.on(:error) do |e|
331
- completion_queue.push(:error)
332
- raise Sdk::Error, "WebSocket error: #{e.message}"
329
+ client.on(:error) do |e|
330
+ completion_queue.push(:error)
331
+ raise Sdk::Error, "WebSocket error: #{e.message}"
332
+ end
333
333
  end
334
334
 
335
335
  # Wait for completion
@@ -448,18 +448,21 @@ module Daytona
448
448
  url.scheme = url.scheme == 'https' ? 'wss' : 'ws'
449
449
  url.path = "/process/pty/#{session_id}/connect"
450
450
 
451
- PtyHandle.new(
452
- WebSocket::Client::Simple.connect(
453
- url.to_s,
454
- headers: toolbox_api.api_client.default_headers.dup.merge(
455
- 'X-Daytona-Preview-Token' => preview_link.token
456
- )
457
- ),
458
- session_id:,
459
-
460
- handle_resize: ->(pty_size) { resize_pty_session(session_id, pty_size) },
461
- handle_kill: -> { delete_pty_session(session_id) }
462
- ).tap(&:wait_for_connection)
451
+ handle = nil
452
+ WebSocket::Client::Simple.connect(
453
+ url.to_s,
454
+ headers: toolbox_api.api_client.default_headers.dup.merge(
455
+ 'X-Daytona-Preview-Token' => preview_link.token
456
+ )
457
+ ) do |client|
458
+ handle = PtyHandle.new(
459
+ client,
460
+ session_id:,
461
+ handle_resize: ->(pty_size) { resize_pty_session(session_id, pty_size) },
462
+ handle_kill: -> { delete_pty_session(session_id) }
463
+ )
464
+ end
465
+ handle.tap(&:wait_for_connection)
463
466
  end
464
467
 
465
468
  # Resizes a PTY session to the specified dimensions
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.184.0'
8
+ VERSION = '0.185.0'
9
9
  end
10
10
  end
@@ -111,6 +111,7 @@ module Daytona
111
111
  end
112
112
 
113
113
  create_snapshot_req.region_id = params.region_id || @default_region_id
114
+ create_snapshot_req.sandbox_class = params.sandbox_class
114
115
 
115
116
  snapshot = snapshots_api.create_snapshot(create_snapshot_req)
116
117
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: daytona
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.184.0
4
+ version: 0.185.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daytona Platforms Inc.
@@ -85,28 +85,28 @@ dependencies:
85
85
  requirements:
86
86
  - - '='
87
87
  - !ruby/object:Gem::Version
88
- version: 0.184.0
88
+ version: 0.185.0
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - '='
94
94
  - !ruby/object:Gem::Version
95
- version: 0.184.0
95
+ version: 0.185.0
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: daytona_toolbox_api_client
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 0.184.0
102
+ version: 0.185.0
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - '='
108
108
  - !ruby/object:Gem::Version
109
- version: 0.184.0
109
+ version: 0.185.0
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: dotenv
112
112
  requirement: !ruby/object:Gem::Requirement