daytona 0.165.0 → 0.167.0.alpha.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.
@@ -1,16 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'base64'
4
- require 'json'
5
3
  require 'uri'
6
4
 
7
5
  module Daytona
8
6
  class Process # rubocop:disable Metrics/ClassLength
9
7
  include Instrumentation
10
8
 
11
- # @return [Daytona::SandboxPythonCodeToolbox,
12
- attr_reader :code_toolbox
13
-
14
9
  # @return [String] The ID of the Sandbox
15
10
  attr_reader :sandbox_id
16
11
 
@@ -20,18 +15,21 @@ module Daytona
20
15
  # @return [Proc] Function to get preview link for a port
21
16
  attr_reader :get_preview_link
22
17
 
18
+ # @return [String] The language for code execution (e.g. 'python', 'typescript', 'javascript')
19
+ attr_reader :language
20
+
23
21
  # Initialize a new Process instance
24
22
  #
25
- # @param code_toolbox [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
26
23
  # @param sandbox_id [String] The ID of the Sandbox
27
24
  # @param toolbox_api [DaytonaToolboxApiClient::ProcessApi] API client for Sandbox operations
28
25
  # @param get_preview_link [Proc] Function to get preview link for a port
26
+ # @param language [String] The language for code execution
29
27
  # @param otel_state [Daytona::OtelState, nil]
30
- def initialize(code_toolbox:, sandbox_id:, toolbox_api:, get_preview_link:, otel_state: nil)
31
- @code_toolbox = code_toolbox
28
+ def initialize(sandbox_id:, toolbox_api:, get_preview_link:, language: 'python', otel_state: nil)
32
29
  @sandbox_id = sandbox_id
33
30
  @toolbox_api = toolbox_api
34
31
  @get_preview_link = get_preview_link
32
+ @language = language
35
33
  @otel_state = otel_state
36
34
  end
37
35
 
@@ -54,29 +52,16 @@ module Daytona
54
52
  #
55
53
  # # Command with timeout
56
54
  # result = sandbox.process.exec("sleep 10", timeout: 5)
57
- def exec(command:, cwd: nil, env: nil, timeout: nil) # rubocop:disable Metrics/MethodLength
58
- if env && !env.empty?
59
- env.each_key do |key|
60
- unless key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
61
- raise ArgumentError,
62
- "Invalid environment variable name: '#{key}'"
63
- end
64
- end
65
- safe_env_exports = env.map do |key, value|
66
- "export #{key}=\"$(printf '%s' '#{Base64.strict_encode64(value)}' | base64 -d)\""
67
- end.join('; ')
68
- command = "#{safe_env_exports}; #{command}"
69
- end
70
-
71
- response = toolbox_api.execute_command(DaytonaToolboxApiClient::ExecuteRequest.new(command:, cwd:, timeout:))
72
- # Post-process the output to extract ExecutionArtifacts
73
- artifacts = parse_output(response.result.split("\n", -1))
55
+ def exec(command:, cwd: nil, env: nil, timeout: nil)
56
+ envs = env&.empty? ? nil : env
74
57
 
75
- # Create new response with processed output and charts
58
+ response = toolbox_api.execute_command(DaytonaToolboxApiClient::ExecuteRequest.new(command:, cwd:, envs:,
59
+ timeout:))
60
+ result = response.result || ''
76
61
  ExecuteResponse.new(
77
62
  exit_code: response.exit_code,
78
- result: artifacts.stdout,
79
- artifacts: artifacts
63
+ result:,
64
+ artifacts: ExecutionArtifacts.new(result, [])
80
65
  )
81
66
  end
82
67
 
@@ -96,7 +81,19 @@ module Daytona
96
81
  # CODE
97
82
  # puts response.artifacts.stdout # Prints: Sum: 30
98
83
  def code_run(code:, params: nil, timeout: nil)
99
- exec(command: code_toolbox.get_run_command(code, params), env: params&.env, timeout:)
84
+ response = toolbox_api.code_run(
85
+ DaytonaToolboxApiClient::CodeRunRequest.new(
86
+ code:, language:, argv: params&.argv, envs: params&.env, timeout:
87
+ )
88
+ )
89
+
90
+ ExecuteResponse.new(
91
+ exit_code: response.exit_code,
92
+ result: response.result,
93
+ artifacts: ExecutionArtifacts.new(response.result, (response.artifacts&.charts || []).map do |c|
94
+ Charts.parse_chart(c)
95
+ end)
96
+ )
100
97
  end
101
98
 
102
99
  # Creates a new long-running background session in the Sandbox
@@ -186,15 +183,12 @@ module Daytona
186
183
  suppress_input_echo: req.suppress_input_echo)
187
184
  )
188
185
 
189
- stdout, stderr = Util.demux(response.output || '')
190
-
191
186
  SessionExecuteResponse.new(
192
187
  cmd_id: response.cmd_id,
193
188
  output: response.output,
194
- stdout:,
195
- stderr:,
189
+ stdout: response.stdout || '',
190
+ stderr: response.stderr || '',
196
191
  exit_code: response.exit_code,
197
- # TODO: DaytonaApiClient::SessionExecuteResponse doesn't have additional_properties attribute
198
192
  additional_properties: {}
199
193
  )
200
194
  end
@@ -210,12 +204,8 @@ module Daytona
210
204
  # puts "Command stdout: #{logs.stdout}"
211
205
  # puts "Command stderr: #{logs.stderr}"
212
206
  def get_session_command_logs(session_id:, command_id:)
213
- parse_session_command_logs(
214
- toolbox_api.get_session_command_logs(
215
- session_id,
216
- command_id
217
- )
218
- )
207
+ response = toolbox_api.get_session_command_logs(session_id, command_id)
208
+ SessionCommandLogsResponse.new(output: response.output, stdout: response.stdout, stderr: response.stderr)
219
209
  end
220
210
 
221
211
  # Asynchronously retrieves and processes the logs for a command executed in a session as they become available
@@ -284,7 +274,10 @@ module Daytona
284
274
  # logs = sandbox.process.get_entrypoint_logs()
285
275
  # puts "Command stdout: #{logs.stdout}"
286
276
  # puts "Command stderr: #{logs.stderr}"
287
- def get_entrypoint_logs = parse_session_command_logs(toolbox_api.get_entrypoint_logs)
277
+ def get_entrypoint_logs
278
+ response = toolbox_api.get_entrypoint_logs
279
+ SessionCommandLogsResponse.new(output: response.output, stdout: response.stdout, stderr: response.stderr)
280
+ end
288
281
 
289
282
  # Asynchronously retrieves and processes the sandbox entrypoint logs as they become available
290
283
  #
@@ -542,64 +535,7 @@ module Daytona
542
535
  # @return [Daytona::OtelState, nil]
543
536
  attr_reader :otel_state
544
537
 
545
- # Parse the output of a command to extract ExecutionArtifacts
546
- #
547
- # @param lines [Array<String>] A list of lines of output from a command
548
- # @return [Daytona::ExecutionArtifacts] The artifacts from the command execution
549
- def parse_output(lines)
550
- artifacts = ExecutionArtifacts.new('', [])
551
- stdout_lines = []
552
-
553
- lines.each do |line|
554
- if line.start_with?(ARTIFACT_PREFIX)
555
- parse_json_line(line:, artifacts:)
556
- else
557
- stdout_lines << line
558
- end
559
- end
560
-
561
- artifacts.stdout = stdout_lines.join("\n")
562
- artifacts
563
- end
564
-
565
- # Parse a JSON line to extract artifacts
566
- #
567
- # @param line [String] The line to parse
568
- # @param artifacts [Daytona::ExecutionArtifacts] The artifacts to add to
569
- # @return [void]
570
- def parse_json_line(line:, artifacts:)
571
- data = JSON.parse(line.sub(ARTIFACT_PREFIX, '').strip, symbolize_names: true)
572
-
573
- case data.fetch(:type, nil)
574
- when ArtifactType::CHART
575
- artifacts.charts.append(Charts.parse(data.fetch(:value, {})))
576
- end
577
- end
578
-
579
- # Parse combined stdout/stderr output into separate streams
580
- #
581
- # @param data [String] Combined log string with STDOUT_PREFIX and STDERR_PREFIX markers
582
- # @return [SessionCommandLogsResponse] Response with separated stdout and stderr
583
- def parse_session_command_logs(data)
584
- stdout, stderr = Util.demux(data)
585
-
586
- SessionCommandLogsResponse.new(
587
- output: data,
588
- stdout:,
589
- stderr:
590
- )
591
- end
592
-
593
- ARTIFACT_PREFIX = 'dtn_artifact_k39fd2:'
594
- private_constant :ARTIFACT_PREFIX
595
-
596
538
  WS_PORT = 2280
597
539
  private_constant :WS_PORT
598
-
599
- module ArtifactType
600
- ALL = [
601
- CHART = 'chart'
602
- ].freeze
603
- end
604
540
  end
605
541
  end
@@ -90,9 +90,6 @@ module Daytona
90
90
  # @return [String] The version of the daemon running in the sandbox
91
91
  attr_reader :daemon_version
92
92
 
93
- # @return [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
94
- attr_reader :code_toolbox
95
-
96
93
  # @return [Daytona::Config]
97
94
  attr_reader :config
98
95
 
@@ -114,14 +111,12 @@ module Daytona
114
111
  # @return [Daytona::CodeInterpreter]
115
112
  attr_reader :code_interpreter
116
113
 
117
- # @params code_toolbox [Daytona::SandboxPythonCodeToolbox, Daytona::SandboxTsCodeToolbox]
118
114
  # @params config [Daytona::Config]
119
115
  # @params sandbox_api [DaytonaApiClient::SandboxApi]
120
116
  # @params sandbox_dto [DaytonaApiClient::Sandbox]
121
117
  # @params otel_state [Daytona::OtelState, nil]
122
- def initialize(code_toolbox:, sandbox_dto:, config:, sandbox_api:, otel_state: nil) # rubocop:disable Metrics/MethodLength
118
+ def initialize(sandbox_dto:, config:, sandbox_api:, otel_state: nil) # rubocop:disable Metrics/MethodLength
123
119
  process_response(sandbox_dto)
124
- @code_toolbox = code_toolbox
125
120
  @config = config
126
121
  @sandbox_api = sandbox_api
127
122
  @otel_state = otel_state
@@ -150,9 +145,9 @@ module Daytona
150
145
 
151
146
  @process = Process.new(
152
147
  sandbox_id: id,
153
- code_toolbox:,
154
148
  toolbox_api: process_api,
155
149
  get_preview_link: proc { |port| preview_url(port) },
150
+ language: (labels || {}).fetch(CODE_TOOLBOX_LANGUAGE_LABEL, 'python'),
156
151
  otel_state:
157
152
  )
158
153
  @fs = FileSystem.new(sandbox_id: id, toolbox_api: fs_api, otel_state:)
@@ -465,12 +460,59 @@ module Daytona
465
460
  DaytonaApiClient::SandboxState::DESTROYED])
466
461
  end
467
462
 
463
+ # Forks the Sandbox, creating a new Sandbox with an identical filesystem.
464
+ # The forked Sandbox is a copy-on-write clone of the original. It starts
465
+ # with the same disk contents but operates independently from that point on.
466
+ #
467
+ # @param name [String, nil] Optional name for the forked Sandbox
468
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
469
+ # @return [Daytona::Sandbox] The forked Sandbox
470
+ def experimental_fork(name: nil, timeout: DEFAULT_TIMEOUT) # rubocop:disable Metrics/MethodLength
471
+ forked_dto = nil
472
+ with_timeout(
473
+ timeout:,
474
+ message: "Sandbox #{id} fork failed to become ready within the #{timeout} seconds timeout period",
475
+ setup: proc {
476
+ forked_dto = sandbox_api.fork_sandbox(id, DaytonaApiClient::ForkSandbox.new(name:))
477
+ }
478
+ ) do
479
+ forked = Sandbox.new(
480
+ sandbox_dto: forked_dto,
481
+ config:,
482
+ sandbox_api:,
483
+ code_toolbox:,
484
+ otel_state:
485
+ )
486
+ forked.send(:wait_for_states, operation: OPERATION_START,
487
+ target_states: [DaytonaApiClient::SandboxState::STARTED])
488
+ return forked
489
+ end
490
+ end
491
+
492
+ # Creates a snapshot from the current state of the Sandbox.
493
+ # The Sandbox will temporarily enter a 'snapshotting' state and return to its previous state when complete.
494
+ #
495
+ # @param name [String] Name for the new snapshot
496
+ # @param timeout [Numeric] Maximum wait time in seconds (defaults to 60 s)
497
+ # @return [void]
498
+ def experimental_create_snapshot(name:, timeout: DEFAULT_TIMEOUT)
499
+ with_timeout(
500
+ timeout:,
501
+ message: "Sandbox #{id} snapshot failed within the #{timeout} seconds timeout period",
502
+ setup: proc {
503
+ sandbox_api.create_sandbox_snapshot(id, DaytonaApiClient::CreateSandboxSnapshot.new(name:))
504
+ refresh
505
+ }
506
+ ) { wait_for_snapshot_complete }
507
+ end
508
+
468
509
  instrument :archive, :auto_archive_interval=, :auto_delete_interval=, :auto_stop_interval=,
469
510
  :create_ssh_access, :delete, :get_user_home_dir, :get_work_dir, :labels=,
470
511
  :preview_url, :create_signed_preview_url, :expire_signed_preview_url,
471
512
  :refresh, :refresh_activity, :revoke_ssh_access, :start, :recover, :stop,
472
513
  :create_lsp_server, :validate_ssh_access, :wait_for_sandbox_start,
473
514
  :wait_for_sandbox_stop, :resize, :wait_for_resize_complete,
515
+ :experimental_fork, :experimental_create_snapshot,
474
516
  component: 'Sandbox'
475
517
 
476
518
  private
@@ -593,5 +635,25 @@ module Daytona
593
635
 
594
636
  OPERATION_RESIZE = :resize
595
637
  private_constant :OPERATION_RESIZE
638
+
639
+ def wait_for_snapshot_complete
640
+ interval = INITIAL_POLL_INTERVAL
641
+ start_time = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
642
+ while state == DaytonaApiClient::SandboxState::SNAPSHOTTING
643
+ refresh
644
+
645
+ if [DaytonaApiClient::SandboxState::ERROR, DaytonaApiClient::SandboxState::BUILD_FAILED].include?(state)
646
+ raise Sdk::Error,
647
+ "Sandbox #{id} snapshot failed with state: #{state}, error reason: #{error_reason}"
648
+ end
649
+
650
+ break if state != DaytonaApiClient::SandboxState::SNAPSHOTTING
651
+
652
+ sleep(interval)
653
+ if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time > 5
654
+ interval = [interval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL].min
655
+ end
656
+ end
657
+ end
596
658
  end
597
659
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Daytona
4
4
  module Sdk
5
- VERSION = '0.165.0'
5
+ VERSION = '0.167.0.alpha.1'
6
6
  end
7
7
  end
data/lib/daytona/sdk.rb CHANGED
@@ -24,9 +24,6 @@ require_relative 'common/response'
24
24
  require_relative 'common/snapshot'
25
25
  require_relative 'code_interpreter'
26
26
  require_relative 'computer_use'
27
- require_relative 'code_toolbox/sandbox_python_code_toolbox'
28
- require_relative 'code_toolbox/sandbox_ts_code_toolbox'
29
- require_relative 'code_toolbox/sandbox_js_code_toolbox'
30
27
  require_relative 'daytona'
31
28
  require_relative 'file_system'
32
29
  require_relative 'git'
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.165.0
4
+ version: 0.167.0.alpha.1
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.165.0
88
+ version: 0.167.0.alpha.1
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.165.0
95
+ version: 0.167.0.alpha.1
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.165.0
102
+ version: 0.167.0.alpha.1
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.165.0
109
+ version: 0.167.0.alpha.1
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: dotenv
112
112
  requirement: !ruby/object:Gem::Requirement
@@ -179,9 +179,6 @@ files:
179
179
  - Rakefile
180
180
  - lib/daytona.rb
181
181
  - lib/daytona/code_interpreter.rb
182
- - lib/daytona/code_toolbox/sandbox_js_code_toolbox.rb
183
- - lib/daytona/code_toolbox/sandbox_python_code_toolbox.rb
184
- - lib/daytona/code_toolbox/sandbox_ts_code_toolbox.rb
185
182
  - lib/daytona/common/charts.rb
186
183
  - lib/daytona/common/code_interpreter.rb
187
184
  - lib/daytona/common/code_language.rb
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'base64'
4
-
5
- module Daytona
6
- class SandboxJsCodeToolbox
7
- def get_run_command(code, params = nil)
8
- # Prepend argv fix: node - places '-' at argv[1]; splice it out to match legacy node -e behaviour
9
- # Encode the provided code in base64
10
- base64_code = Base64.strict_encode64("process.argv.splice(1, 1);\n" + code)
11
-
12
- # Build command-line arguments string
13
- argv = ''
14
- argv = params.argv.join(' ') if params&.argv && !params.argv.empty?
15
-
16
- # Pipe the base64-encoded code via stdin to avoid OS ARG_MAX limits on large payloads
17
- # Use node - to read from stdin (node /dev/stdin does not work when stdin is a pipe)
18
- "printf '%s' '#{base64_code}' | base64 -d | node - #{argv}"
19
- end
20
- end
21
- end