daytona 0.198.0 → 0.200.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: e2003b4293b8ee69d171ad86402151e2924b814022649097deb51da19da6ff58
4
- data.tar.gz: 30884a506c2dd332c804f995dd2d3afc654cf75b0fa66e4bf54cdd4a38909c12
3
+ metadata.gz: de70d9e56ce97468d9ef3bcf8c7dbe0b70f74ee7e64a7ac1d0e8642857a3c257
4
+ data.tar.gz: 3cec447b03a65f42bcd84c49e719cd8771a1404f496384afd5e797b429e47fb1
5
5
  SHA512:
6
- metadata.gz: f4627f864da4ae028a19d856a014da0f3513e30a5a8c7299148d89d815a82b13a2c0de3bc8fc3dc4e96ca7a455ee9ef9e291fe71dd58f57e12af8e85ebcbd3d1
7
- data.tar.gz: c8b07b46dfb64043794af1d4d33db1ca3ac49c1271ebde8eae3b2c9e05ca66030e780d2fbb5cede035c564c03a69f0abba68d87ff098bac36d198fe3ab1d2636
6
+ metadata.gz: 91c94980be6003996bbbf4b447b170e9165cbc3a47e78c47f91886f28accd5f7b083b04903fc59dede9bbe56ccdcaf9957da73e07202e762ca928203274b2c8b
7
+ data.tar.gz: db39e610c03a011d4c2468475393ca7b7bd17cc1087dbb0edd46c9fc6959da18e0ca61d861485ca0b27faa8ccb4061f631ec44c97d90d8e6bea7db75317db4e3
@@ -56,6 +56,9 @@ module Daytona
56
56
  # @return [String, nil] Comma-separated list of allowed domains for the Sandbox
57
57
  attr_accessor :domain_allow_list
58
58
 
59
+ # @return [Integer, nil] Time to live in minutes (0 to disable)
60
+ attr_accessor :ttl_minutes
61
+
59
62
  # @return [Boolean, nil] Whether the Sandbox should be ephemeral
60
63
  attr_accessor :ephemeral
61
64
 
@@ -83,6 +86,7 @@ module Daytona
83
86
  # @param network_block_all [Boolean, nil] Whether to block all network access for the Sandbox
84
87
  # @param network_allow_list [String, nil] Comma-separated list of allowed CIDR network addresses for the Sandbox
85
88
  # @param domain_allow_list [String, nil] Comma-separated list of allowed domains for the Sandbox
89
+ # @param ttl_minutes [Integer, nil] Time to live in minutes (0 to disable)
86
90
  # @param ephemeral [Boolean, nil] Whether the Sandbox should be ephemeral
87
91
  # @param linked_sandbox [String, nil] ID or name of an existing Sandbox to link the new Sandbox to
88
92
  def initialize( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
@@ -96,6 +100,7 @@ module Daytona
96
100
  auto_pause_interval: nil,
97
101
  auto_archive_interval: nil,
98
102
  auto_delete_interval: nil,
103
+ ttl_minutes: nil,
99
104
  volumes: nil,
100
105
  secrets: nil,
101
106
  network_block_all: nil,
@@ -114,6 +119,7 @@ module Daytona
114
119
  @auto_pause_interval = auto_pause_interval
115
120
  @auto_archive_interval = auto_archive_interval
116
121
  @auto_delete_interval = auto_delete_interval
122
+ @ttl_minutes = ttl_minutes
117
123
  @volumes = volumes
118
124
  @secrets = secrets
119
125
  @network_block_all = network_block_all
@@ -141,6 +147,7 @@ module Daytona
141
147
  auto_pause_interval:,
142
148
  auto_archive_interval:,
143
149
  auto_delete_interval:,
150
+ ttl_minutes:,
144
151
  volumes:,
145
152
  secrets:,
146
153
  network_block_all:,
@@ -193,6 +200,7 @@ module Daytona
193
200
  # @param auto_pause_interval [Integer, nil] Auto-pause interval in minutes
194
201
  # @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
195
202
  # @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
203
+ # @param ttl_minutes [Integer, nil] Time to live in minutes (0 to disable)
196
204
  # @param volumes [Array<DaytonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
197
205
  # @param secrets [Hash<String, String>, nil] Organization Secrets to expose in the Sandbox, as a
198
206
  # mapping of env var name to existing Secret name
@@ -235,6 +243,7 @@ module Daytona
235
243
  # @param auto_pause_interval [Integer, nil] Auto-pause interval in minutes
236
244
  # @param auto_archive_interval [Integer, nil] Auto-archive interval in minutes
237
245
  # @param auto_delete_interval [Integer, nil] Auto-delete interval in minutes
246
+ # @param ttl_minutes [Integer, nil] Time to live in minutes (0 to disable)
238
247
  # @param volumes [Array<DaytonaApiClient::SandboxVolume>, nil] List of volumes mounts to attach to the Sandbox
239
248
  # @param secrets [Hash<String, String>, nil] Organization Secrets to expose in the Sandbox, as a
240
249
  # mapping of env var name to existing Secret name
@@ -5,6 +5,7 @@
5
5
 
6
6
  require 'timeout'
7
7
 
8
+ require 'openssl'
8
9
  require 'websocket-client-simple'
9
10
  require 'json'
10
11
  require 'uri'
@@ -52,6 +53,8 @@ module Daytona
52
53
  @connected = false
53
54
  @mutex = Mutex.new
54
55
  @write_mutex = Mutex.new
56
+ @verify_mutex = Mutex.new
57
+ @tls_verified = nil
55
58
  @health_thread = nil
56
59
  @ping_interval = 25
57
60
  @ping_timeout = 20
@@ -67,13 +70,24 @@ module Daytona
67
70
  ws_url = build_ws_url
68
71
  connected_queue = Queue.new
69
72
 
73
+ # Reset per-connection TLS state so a reconnect re-verifies its new socket
74
+ # instead of reusing a cached result from the previous connection.
75
+ @verify_mutex.synchronize { @tls_verified = nil }
76
+
70
77
  # Capture self because websocket-client-simple uses instance_exec for callbacks
71
78
  client = self
72
79
 
73
80
  # Register callbacks inside the connect block so they are installed before the
74
81
  # socket dials — otherwise the Engine.IO handshake can arrive before :message
75
82
  # is bound, stalling the connection into a polling fallback/timeout.
76
- @ws = WebSocket::Client::Simple.connect(ws_url) do |ws|
83
+ #
84
+ # verify_mode is passed explicitly: websocket-client-simple builds its own
85
+ # SSLContext and defaults to no certificate verification (VERIFY_NONE).
86
+ @ws = WebSocket::Client::Simple.connect(ws_url, verify_mode: OpenSSL::SSL::VERIFY_PEER) do |ws|
87
+ # Capture the client before its reader thread starts so #verify_tls_peer!
88
+ # can reach the SSL socket even when a frame arrives before .connect returns.
89
+ @ws = ws
90
+
77
91
  ws.on :message do |msg|
78
92
  client.send(:handle_raw_message, msg.data.to_s, connected_queue)
79
93
  end
@@ -104,15 +118,26 @@ module Daytona
104
118
  end
105
119
  end
106
120
 
107
- # Wait for connection with timeout
108
121
  result = nil
109
122
  begin
110
- Timeout.timeout(@connect_timeout) { result = connected_queue.pop }
123
+ Timeout.timeout(@connect_timeout) do
124
+ ensure_tls_verified!(connected_queue)
125
+ result = connected_queue.pop
126
+ if result == :open
127
+ send_connect_auth
128
+ result = connected_queue.pop
129
+ end
130
+ end
111
131
  rescue Timeout::Error
112
132
  close
113
133
  raise "WebSocket connection timed out after #{@connect_timeout}s"
114
134
  end
115
135
 
136
+ if result.is_a?(OpenSSL::SSL::SSLError)
137
+ close
138
+ raise result
139
+ end
140
+
116
141
  if result != :connected
117
142
  close
118
143
  raise "WebSocket connection failed: #{result}"
@@ -141,6 +166,46 @@ module Daytona
141
166
 
142
167
  private
143
168
 
169
+ # Verify the TLS peer's hostname exactly once, before any inbound frame is
170
+ # processed and before the auth frame is sent. websocket-client-simple
171
+ # verifies the certificate chain (VERIFY_PEER) during the handshake but never
172
+ # checks that the certificate matches the host, and its reader thread starts
173
+ # before #connect regains control — so a certificate that is valid for the
174
+ # wrong host could otherwise inject Socket.IO events (and receive the CONNECT
175
+ # auth frame) before verification runs. Gating both the reader path and the
176
+ # connect path guarantees no frame reaches the dispatcher, and no credentials
177
+ # are sent, until the peer is verified.
178
+ # @return [Boolean] true when verified (or plaintext ws://, no TLS peer to
179
+ # check); false on failure, after signalling the connecting thread.
180
+ def ensure_tls_verified!(connected_queue)
181
+ @verify_mutex.synchronize do
182
+ return @tls_verified unless @tls_verified.nil?
183
+
184
+ begin
185
+ verify_tls_peer!
186
+ @tls_verified = true
187
+ rescue OpenSSL::SSL::SSLError => e
188
+ @tls_verified = false
189
+ @mutex.synchronize { @connected = false }
190
+ connected_queue&.push(e)
191
+ end
192
+
193
+ @tls_verified
194
+ end
195
+ end
196
+
197
+ def verify_tls_peer!
198
+ socket = @ws&.instance_variable_get(:@socket)
199
+ return unless socket.is_a?(OpenSSL::SSL::SSLSocket)
200
+
201
+ socket.post_connection_check(URI.parse(@api_url).host)
202
+ end
203
+
204
+ def send_connect_auth
205
+ auth = JSON.generate({ token: @token })
206
+ send_raw("#{EIO_MESSAGE}#{SIO_CONNECT}#{auth}")
207
+ end
208
+
144
209
  def build_ws_url
145
210
  parsed = URI.parse(@api_url)
146
211
  ws_scheme = parsed.scheme == 'https' ? 'wss' : 'ws'
@@ -160,6 +225,11 @@ module Daytona
160
225
  def handle_raw_message(raw, connected_queue)
161
226
  return if raw.nil? || raw.empty?
162
227
 
228
+ # Drop every frame from a peer whose hostname is not yet verified. The
229
+ # reader thread can run before #connect verifies, so this is the gate that
230
+ # stops a wrong-host peer from injecting events or eliciting the auth frame.
231
+ return unless ensure_tls_verified!(connected_queue)
232
+
163
233
  # Track all server activity for health monitoring.
164
234
  # If the server stops sending ANY data (pings, events, etc.)
165
235
  # the health monitor will detect the dead connection.
@@ -175,9 +245,10 @@ module Daytona
175
245
  rescue JSON::ParserError
176
246
  # Use default ping interval
177
247
  end
178
- # Send Socket.IO CONNECT with auth
179
- auth = JSON.generate({ token: @token })
180
- send_raw("#{EIO_MESSAGE}#{SIO_CONNECT}#{auth}")
248
+ # Signal the connecting thread instead of sending the CONNECT auth frame
249
+ # here: the token must not leave this process until the connecting thread
250
+ # has completed TLS peer verification (see #connect).
251
+ connected_queue&.push(:open)
181
252
 
182
253
  when EIO_PING
183
254
  # Server heartbeat — respond immediately with PONG
@@ -99,7 +99,7 @@ module Daytona
99
99
  #
100
100
  # @param params [Daytona::CreateSandboxFromSnapshotParams, Daytona::CreateSandboxFromImageParams, Nil] Sandbox creation parameters
101
101
  # @return [Daytona::Sandbox] The created sandbox
102
- # @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, or auto_archive_interval is negative,
102
+ # @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, auto_archive_interval, or ttl_minutes is negative,
103
103
  # or if auto_stop_interval and auto_pause_interval are both non-zero
104
104
  def create(params = nil, on_snapshot_create_logs: nil)
105
105
  if params.nil?
@@ -237,7 +237,7 @@ module Daytona
237
237
  # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s).
238
238
  # @param on_snapshot_create_logs [Proc]
239
239
  # @return [Daytona::Sandbox] The created sandbox
240
- # @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, or auto_archive_interval is negative,
240
+ # @raise [Daytona::Sdk::Error] If auto_stop_interval, auto_pause_interval, auto_archive_interval, or ttl_minutes is negative,
241
241
  # or if auto_stop_interval and auto_pause_interval are both non-zero
242
242
  def _create(params, timeout: 60, on_snapshot_create_logs: nil)
243
243
  raise Sdk::Error, 'Timeout must be a non-negative number' if timeout.negative?
@@ -261,6 +261,8 @@ module Daytona
261
261
  raise Sdk::Error, 'auto_archive_interval must be a non-negative integer'
262
262
  end
263
263
 
264
+ raise Sdk::Error, 'ttl_minutes must be a non-negative integer' if params.ttl_minutes&.negative?
265
+
264
266
  labels = params.labels&.dup || {}
265
267
  labels[CODE_TOOLBOX_LANGUAGE_LABEL] = params.language.to_s if params.language
266
268
 
@@ -274,6 +276,7 @@ module Daytona
274
276
  auto_pause_interval: params.auto_pause_interval,
275
277
  auto_archive_interval: params.auto_archive_interval,
276
278
  auto_delete_interval: params.auto_delete_interval,
279
+ ttl_minutes: params.ttl_minutes,
277
280
  volumes: params.volumes,
278
281
  secrets: params.secrets&.map { |env_var, secret_name| { env_var.to_s => secret_name.to_s } },
279
282
  network_block_all: params.network_block_all,
@@ -140,6 +140,9 @@ module Daytona
140
140
  # Not returned by list results; call #refresh on each item to populate.
141
141
  attr_reader :build_info
142
142
 
143
+ # @return [String, nil] When the sandbox will be automatically destroyed (nil if no TTL is set)
144
+ attr_reader :auto_destroy_at
145
+
143
146
  # @return [String] The creation timestamp of the sandbox
144
147
  attr_reader :created_at
145
148
 
@@ -354,6 +357,19 @@ module Daytona
354
357
  @auto_pause_interval = interval
355
358
  end
356
359
 
360
+ # Sets the TTL (time to live) for the Sandbox.
361
+ # The TTL is re-anchored from the current time. When it elapses the Sandbox is destroyed,
362
+ # regardless of its current state. Use 0 to disable.
363
+ #
364
+ # @param minutes [Integer]
365
+ # @return [void]
366
+ # @raise [Daytona::Sdk::Error]
367
+ def ttl_minutes=(minutes)
368
+ raise Sdk::Error, 'TTL must be a non-negative integer' if minutes.negative?
369
+
370
+ sandbox_api.set_ttl(id, minutes)
371
+ end
372
+
357
373
  # Creates an SSH access token for the sandbox.
358
374
  #
359
375
  # @param expires_in_minutes [Integer] TThe number of minutes the SSH access token will be valid for
@@ -821,6 +837,7 @@ module Daytona
821
837
  :experimental_fork, :experimental_create_snapshot, :pause
822
838
 
823
839
  instrument :archive, :auto_archive_interval=, :auto_delete_interval=, :auto_pause_interval=, :auto_stop_interval=,
840
+ :ttl_minutes=,
824
841
  :update_network_settings, :update_secrets, :update_env,
825
842
  :create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :get_metrics, :get_metrics_latest,
826
843
  :labels=,
@@ -973,6 +990,7 @@ module Daytona
973
990
  @auto_pause_interval = sandbox_dto.auto_pause_interval
974
991
  @auto_archive_interval = sandbox_dto.auto_archive_interval
975
992
  @auto_delete_interval = sandbox_dto.auto_delete_interval
993
+ @auto_destroy_at = sandbox_dto.auto_destroy_at
976
994
  @created_at = sandbox_dto.created_at
977
995
  @updated_at = sandbox_dto.updated_at
978
996
  @last_activity_at = sandbox_dto.last_activity_at
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.198.0'
8
+ VERSION = '0.200.0'
9
9
  end
10
10
  end
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.198.0
4
+ version: 0.200.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daytona Platforms Inc.
@@ -85,42 +85,42 @@ dependencies:
85
85
  requirements:
86
86
  - - '='
87
87
  - !ruby/object:Gem::Version
88
- version: 0.198.0
88
+ version: 0.200.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.198.0
95
+ version: 0.200.0
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: daytona_api_client
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 0.198.0
102
+ version: 0.200.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.198.0
109
+ version: 0.200.0
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: daytona_toolbox_api_client
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - '='
115
115
  - !ruby/object:Gem::Version
116
- version: 0.198.0
116
+ version: 0.200.0
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - '='
122
122
  - !ruby/object:Gem::Version
123
- version: 0.198.0
123
+ version: 0.200.0
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: dotenv
126
126
  requirement: !ruby/object:Gem::Requirement