microsandbox-rb 0.17.0 → 0.17.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.
@@ -38,7 +38,7 @@ module Microsandbox
38
38
 
39
39
  # @return [Time, nil]
40
40
  def created_at
41
- @created_at_ms && Time.at(@created_at_ms / 1000.0)
41
+ Microsandbox.time_from_ms(@created_at_ms)
42
42
  end
43
43
 
44
44
  # A host-side filesystem view over this volume (read/write its contents
@@ -55,8 +55,10 @@ module Microsandbox
55
55
 
56
56
  # A host-side filesystem view over a named volume, from {Volume.fs} or
57
57
  # {VolumeInfo#fs}. Reads and writes the volume's contents directly on the host,
58
- # without booting a sandbox. All paths are relative to the volume root. Mirrors
59
- # the `VolumeFs` of the official Python/Node SDKs.
58
+ # without booting a sandbox. All paths are relative to the volume root (a
59
+ # leading `/` is the volume root) and may not contain a `..` segment — every
60
+ # operation raises {FilesystemError} for one. Mirrors the `VolumeFs` of the
61
+ # official Python/Node SDKs.
60
62
  class VolumeFs
61
63
  def initialize(native)
62
64
  @native = native
@@ -65,13 +67,13 @@ module Microsandbox
65
67
  # Read a file as raw bytes (ASCII-8BIT).
66
68
  # @return [String]
67
69
  def read(path)
68
- @native.read(path.to_s)
70
+ @native.read(contained(path))
69
71
  end
70
72
 
71
73
  # Read a file as a UTF-8 string.
72
74
  # @return [String]
73
75
  def read_text(path)
74
- @native.read_text(path.to_s)
76
+ @native.read_text(contained(path))
75
77
  end
76
78
 
77
79
  # Write data to a file, creating parent directories as needed.
@@ -80,60 +82,75 @@ module Microsandbox
80
82
  # @return [nil]
81
83
  def write(path, data)
82
84
  bytes = Microsandbox.coerce_write_bytes(data)
83
- @native.write(path.to_s, bytes)
85
+ @native.write(contained(path), bytes)
84
86
  nil
85
87
  end
86
88
 
87
89
  # List the entries of a directory.
88
90
  # @return [Array<FsEntry>]
89
91
  def list(path)
90
- @native.list(path.to_s).map { |entry| FsEntry.new(entry) }
92
+ @native.list(contained(path)).map { |entry| FsEntry.new(entry) }
91
93
  end
92
94
 
93
95
  # Create a directory (and any missing parents).
94
96
  # @return [nil]
95
97
  def mkdir(path)
96
- @native.mkdir(path.to_s)
98
+ @native.mkdir(contained(path))
97
99
  nil
98
100
  end
99
101
 
100
102
  # Remove a single file.
101
103
  # @return [nil]
102
104
  def remove_file(path)
103
- @native.remove_file(path.to_s)
105
+ @native.remove_file(contained(path))
104
106
  nil
105
107
  end
106
108
 
107
109
  # Remove a directory recursively.
108
110
  # @return [nil]
109
111
  def remove_dir(path)
110
- @native.remove_dir(path.to_s)
112
+ @native.remove_dir(contained(path))
111
113
  nil
112
114
  end
113
115
 
114
116
  # @return [Boolean] whether the path exists in the volume
115
117
  def exists?(path)
116
- @native.exists(path.to_s)
118
+ @native.exists(contained(path))
117
119
  end
118
120
 
119
121
  # Copy a file within the volume.
120
122
  # @return [nil]
121
123
  def copy(src, dst)
122
- @native.copy(src.to_s, dst.to_s)
124
+ @native.copy(contained(src), contained(dst))
123
125
  nil
124
126
  end
125
127
 
126
128
  # Rename/move a file or directory within the volume.
127
129
  # @return [nil]
128
130
  def rename(src, dst)
129
- @native.rename(src.to_s, dst.to_s)
131
+ @native.rename(contained(src), contained(dst))
130
132
  nil
131
133
  end
132
134
 
133
135
  # Stat a path.
134
136
  # @return [FsMetadata]
135
137
  def stat(path)
136
- FsMetadata.new(@native.stat(path.to_s))
138
+ FsMetadata.new(@native.stat(contained(path)))
139
+ end
140
+
141
+ private
142
+
143
+ # Volume paths are relative to the volume root and never need a `..`
144
+ # segment, so one is refused here with a clear message instead of being
145
+ # handed to the resolver. The native layer enforces the same rule; this
146
+ # check only exists so the error names the cause.
147
+ def contained(path)
148
+ s = path.to_s
149
+ if s.split("/").include?("..")
150
+ raise Microsandbox::FilesystemError,
151
+ "path traversal outside volume root (`..` segments are not allowed in volume paths)"
152
+ end
153
+ s
137
154
  end
138
155
  end
139
156
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Entry point named after the gem. `require "microsandbox"` resolves to
4
+ # whichever installed gem ships that file first, and the separate
5
+ # `microsandbox` gem on RubyGems ships one too; outside Bundler,
6
+ # `require "microsandbox-rb"` always loads this gem.
7
+ require_relative "microsandbox"
data/lib/microsandbox.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
4
+
3
5
  require_relative "microsandbox/version"
4
6
 
5
7
  # Load the compiled native extension. Precompiled platform gems stage the
@@ -47,6 +49,13 @@ require_relative "microsandbox/sandbox"
47
49
  # puts sb.exec("python", ["-c", "print('Hello, World!')"]).stdout
48
50
  # end
49
51
  module Microsandbox
52
+ # Serializes runtime provisioning within this process: {install}, {setup} and
53
+ # the first-use check in {ensure_runtime!}. The installer replaces files in
54
+ # place, so two threads racing through a first `Sandbox.create` must not both
55
+ # run it. A Monitor (re-entrant) because ensure_runtime! calls install.
56
+ RUNTIME_SETUP_LOCK = Monitor.new
57
+ private_constant :RUNTIME_SETUP_LOCK
58
+
50
59
  class << self
51
60
  # @return [String] the gem version
52
61
  def version
@@ -74,7 +83,7 @@ module Microsandbox
74
83
  # gem supplies the runtime.
75
84
  # @return [nil]
76
85
  def install
77
- Native.install
86
+ RUNTIME_SETUP_LOCK.synchronize { Native.install }
78
87
  nil
79
88
  end
80
89
 
@@ -93,7 +102,7 @@ module Microsandbox
93
102
  opts["version"] = version.to_s if version
94
103
  opts["force"] = true if force
95
104
  opts["skip_verify"] = true if skip_verify
96
- Native.setup(opts)
105
+ RUNTIME_SETUP_LOCK.synchronize { Native.setup(opts) }
97
106
  nil
98
107
  end
99
108
 
@@ -130,34 +139,38 @@ module Microsandbox
130
139
  # @return [nil]
131
140
  def ensure_runtime!
132
141
  return if @runtime_ready
133
- # A cloud backend has no local msb/libkrunfw runtime to provision: skip the
134
- # presence check and the first-use download entirely. Resolving the kind
135
- # uses the same lazy env/profile/config ladder every operation already
136
- # consults, so this adds no work for local hosts (the common case).
137
- return if default_backend_kind == :cloud
138
- # The binaries gem won the resolver: its vendored msb (and the libkrunfw
139
- # beside it) are exactly the version this gem was built for, so there is
140
- # nothing to verify or download. (If `MSB_PATH` overrides it, the user owns
141
- # the runtime and we fall through to the existing behaviour.)
142
- if bundled_runtime_active?
143
- @runtime_ready = true
144
- return
145
- end
146
- # Opted out: the caller manages the runtime out of band, so don't fetch,
147
- # verify, or repair it here. Memoize the decision (the env var is stable for
148
- # the process); the operation resolves `msb` itself and surfaces any problem.
149
- if auto_install_disabled?
150
- @runtime_ready = true
151
- return
152
- end
142
+ RUNTIME_SETUP_LOCK.synchronize do
143
+ # Re-check under the lock: another thread may have finished meanwhile.
144
+ return if @runtime_ready
145
+ # A cloud backend has no local msb/libkrunfw runtime to provision: skip the
146
+ # presence check and the first-use download entirely. Resolving the kind
147
+ # uses the same lazy env/profile/config ladder every operation already
148
+ # consults, so this adds no work for local hosts (the common case).
149
+ return if default_backend_kind == :cloud
150
+ # The binaries gem won the resolver: its vendored msb (and the libkrunfw
151
+ # beside it) are exactly the version this gem was built for, so there is
152
+ # nothing to verify or download. (If `MSB_PATH` overrides it, the user owns
153
+ # the runtime and we fall through to the existing behaviour.)
154
+ if bundled_runtime_active?
155
+ @runtime_ready = true
156
+ return
157
+ end
158
+ # Opted out: the caller manages the runtime out of band, so don't fetch,
159
+ # verify, or repair it here. Memoize the decision (the env var is stable for
160
+ # the process); the operation resolves `msb` itself and surfaces any problem.
161
+ if auto_install_disabled?
162
+ @runtime_ready = true
163
+ return
164
+ end
153
165
 
154
- unless installed?
155
- warn "[microsandbox] runtime (msb + libkrunfw) not found; " \
156
- "downloading to ~/.microsandbox (set MICROSANDBOX_NO_AUTO_INSTALL to skip, " \
157
- "or install the microsandbox-rb-binaries gem to ship it with your bundle)..."
166
+ unless installed?
167
+ warn "[microsandbox] runtime (msb + libkrunfw) not found; " \
168
+ "downloading to ~/.microsandbox (set MICROSANDBOX_NO_AUTO_INSTALL to skip, " \
169
+ "or install the microsandbox-rb-binaries gem to ship it with your bundle)..."
170
+ end
171
+ install
172
+ @runtime_ready = true
158
173
  end
159
- install
160
- @runtime_ready = true
161
174
  nil
162
175
  end
163
176
 
@@ -176,7 +189,9 @@ module Microsandbox
176
189
  # setter is a no-op — use `MSB_PATH` to override a bundled runtime.
177
190
  # @param path [String]
178
191
  # @return [void]
192
+ # @raise [ArgumentError] for nil or an empty path (the slot cannot be cleared)
179
193
  def runtime_path=(path)
194
+ require_runtime_path!(:runtime_path=, path)
180
195
  if @bundled_msb_path && path.to_s != @bundled_msb_path
181
196
  warn "[microsandbox] runtime_path= ignored: the microsandbox-rb-binaries gem already " \
182
197
  "claimed the set-once SDK slot (#{@bundled_msb_path}); set MSB_PATH to override it"
@@ -190,7 +205,9 @@ module Microsandbox
190
205
  # Mirrors {runtime_path=} for libkrunfw.
191
206
  # @param path [String]
192
207
  # @return [void]
208
+ # @raise [ArgumentError] for nil or an empty path (the slot cannot be cleared)
193
209
  def libkrunfw_path=(path)
210
+ require_runtime_path!(:libkrunfw_path=, path)
194
211
  Native.set_runtime_libkrunfw_path(path.to_s)
195
212
  end
196
213
 
@@ -272,6 +289,36 @@ module Microsandbox
272
289
  raise TypeError, "data must be a String (got #{data.class})"
273
290
  end
274
291
 
292
+ # A Time for an epoch-milliseconds Integer from the native layer, or nil.
293
+ # Integer arithmetic on purpose: `Time.at(ms / 1000.0)` goes through a
294
+ # Float and lands one millisecond low for about half of all timestamps.
295
+ # @api private
296
+ # @param ms [Integer, nil]
297
+ # @return [Time, nil]
298
+ def time_from_ms(ms)
299
+ ms && Time.at(ms.div(1000), ms % 1000, :millisecond)
300
+ end
301
+
302
+ # Run the block, then close +resource+ — the shared tail of the block forms
303
+ # that own a resource whose close can fail (a write sink flushes on close).
304
+ # If the block raised, its exception is the one the caller needs: a failure
305
+ # from the close that follows is dropped instead of replacing it. If the
306
+ # block finished (or left via break/throw), a close failure propagates.
307
+ # @api private
308
+ def close_after(resource)
309
+ raised = false
310
+ yield
311
+ rescue Exception # standard:disable Lint/RescueException -- re-raised unchanged
312
+ raised = true
313
+ raise
314
+ ensure
315
+ begin
316
+ resource.close
317
+ rescue
318
+ raise unless raised
319
+ end
320
+ end
321
+
275
322
  private
276
323
 
277
324
  # Auto-provisioning is on by default; any non-empty, non-"0"/"false" value
@@ -358,12 +405,22 @@ module Microsandbox
358
405
  private_constant :TRUSTED_BINARIES_OWNERS
359
406
 
360
407
  # Name of the loaded gem whose files define Microsandbox::Binaries, or nil
361
- # when it came from a bare load path.
408
+ # when it came from a bare load path. Gem roots can nest — a host project
409
+ # that is itself a gem, with its bundle installed under the project
410
+ # directory, is a prefix of every gem in that bundle — so the owner is the
411
+ # gem with the LONGEST matching root, not whichever one is listed first.
362
412
  def bundled_runtime_owner
363
413
  source = Binaries.method(:msb_path).source_location&.first
364
414
  return nil unless source
365
- spec = Gem.loaded_specs.values.find { |s| source.start_with?(File.join(s.full_gem_path, "")) }
366
- spec&.name
415
+ roots = Gem.loaded_specs.values.select { |s| source.start_with?(File.join(s.full_gem_path, "")) }
416
+ roots.max_by { |s| s.full_gem_path.length }&.name
417
+ end
418
+
419
+ # The runtime path slots are set-once in the core and have no "unset": nil
420
+ # or "" would be stored as an empty path for the rest of the process.
421
+ def require_runtime_path!(setter, path)
422
+ return unless path.nil? || path.to_s.empty?
423
+ raise ArgumentError, "#{setter} needs a non-empty path (the slot is set-once and cannot be cleared)"
367
424
  end
368
425
 
369
426
  # Whether the binaries gem's msb is what the resolver actually returns — i.e.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: microsandbox-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.17.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao
@@ -43,6 +43,17 @@ files:
43
43
  - DESIGN.md
44
44
  - LICENSE
45
45
  - README.md
46
+ - SECURITY.md
47
+ - docs/configuration.md
48
+ - docs/errors.md
49
+ - docs/execution.md
50
+ - docs/filesystem.md
51
+ - docs/images.md
52
+ - docs/lifecycle.md
53
+ - docs/observability.md
54
+ - docs/releasing.md
55
+ - docs/runtime.md
56
+ - docs/surface.md
46
57
  - ext/microsandbox/Cargo.toml
47
58
  - ext/microsandbox/extconf.rb
48
59
  - ext/microsandbox/src/agent.rs
@@ -51,6 +62,7 @@ files:
51
62
  - ext/microsandbox/src/error.rs
52
63
  - ext/microsandbox/src/exec.rs
53
64
  - ext/microsandbox/src/fs_stream.rs
65
+ - ext/microsandbox/src/host_path.rs
54
66
  - ext/microsandbox/src/image.rs
55
67
  - ext/microsandbox/src/lib.rs
56
68
  - ext/microsandbox/src/runtime.rs
@@ -59,6 +71,7 @@ files:
59
71
  - ext/microsandbox/src/ssh.rs
60
72
  - ext/microsandbox/src/stream.rs
61
73
  - ext/microsandbox/src/volume.rs
74
+ - lib/microsandbox-rb.rb
62
75
  - lib/microsandbox.rb
63
76
  - lib/microsandbox/agent.rb
64
77
  - lib/microsandbox/backend_info.rb