microsandbox-rb 0.17.2 → 0.17.3

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.
@@ -140,10 +140,13 @@ fn list_dir(dir: String) -> Result<RArray, Error> {
140
140
  fn reindex(dir: Option<String>) -> Result<u64, Error> {
141
141
  let dir: PathBuf = match dir {
142
142
  Some(d) => PathBuf::from(d),
143
- None => microsandbox::default_backend()
144
- .as_local()
145
- .map(|l| l.snapshots_dir())
146
- .unwrap_or_else(|| PathBuf::from(".")),
143
+ None => {
144
+ crate::backend::ensure_this_process_backend()?;
145
+ microsandbox::default_backend()
146
+ .as_local()
147
+ .map(|l| l.snapshots_dir())
148
+ .unwrap_or_else(|| PathBuf::from("."))
149
+ }
147
150
  };
148
151
  let n = block_on(Snapshot::reindex(&dir))?.map_err(error::to_ruby)?;
149
152
  Ok(n as u64)
@@ -21,7 +21,7 @@ use tokio::io::AsyncWriteExt;
21
21
  use tokio::sync::Mutex;
22
22
 
23
23
  use crate::error;
24
- use crate::runtime::{block_on, ruby};
24
+ use crate::runtime::{block_on, ruby, ForkGuarded};
25
25
 
26
26
  /// A core error for a session whose value was already taken (closed). Built
27
27
  /// inside `block_on`, so it must be a plain Rust error, not a Ruby one.
@@ -35,19 +35,19 @@ fn consumed() -> MicrosandboxError {
35
35
 
36
36
  #[magnus::wrap(class = "Microsandbox::Native::SshClient", free_immediately, size)]
37
37
  pub struct SshClient {
38
- inner: Arc<Mutex<Option<CoreSshClient>>>,
38
+ inner: ForkGuarded<Arc<Mutex<Option<CoreSshClient>>>>,
39
39
  }
40
40
 
41
41
  impl SshClient {
42
42
  pub fn from_core(inner: CoreSshClient) -> Self {
43
43
  Self {
44
- inner: Arc::new(Mutex::new(Some(inner))),
44
+ inner: ForkGuarded::new("Microsandbox::SshClient", Arc::new(Mutex::new(Some(inner)))),
45
45
  }
46
46
  }
47
47
 
48
48
  /// Run a command over SSH and collect {status, stdout, stderr}.
49
49
  fn exec(&self, command: String, tty: bool) -> Result<RHash, Error> {
50
- let inner = Arc::clone(&self.inner);
50
+ let inner = Arc::clone(self.inner.get()?);
51
51
  let result: MicrosandboxResult<SshOutput> = block_on(async move {
52
52
  let guard = inner.lock().await;
53
53
  let client = guard.as_ref().ok_or_else(consumed)?;
@@ -59,7 +59,7 @@ impl SshClient {
59
59
  /// Attach the local terminal to an interactive SSH shell; returns the exit
60
60
  /// status. Host-TTY coupled (raw mode); not meaningful without a real tty.
61
61
  fn attach(&self, term: Option<String>, detach_keys: Option<String>) -> Result<i32, Error> {
62
- let inner = Arc::clone(&self.inner);
62
+ let inner = Arc::clone(self.inner.get()?);
63
63
  let result: MicrosandboxResult<i32> = block_on(async move {
64
64
  let guard = inner.lock().await;
65
65
  let client = guard.as_ref().ok_or_else(consumed)?;
@@ -80,7 +80,7 @@ impl SshClient {
80
80
 
81
81
  /// Open an SFTP session over this SSH connection.
82
82
  fn sftp(&self) -> Result<SftpClient, Error> {
83
- let inner = Arc::clone(&self.inner);
83
+ let inner = Arc::clone(self.inner.get()?);
84
84
  let result: MicrosandboxResult<CoreSftp> = block_on(async move {
85
85
  let guard = inner.lock().await;
86
86
  let client = guard.as_ref().ok_or_else(consumed)?;
@@ -91,7 +91,7 @@ impl SshClient {
91
91
 
92
92
  /// Close the SSH client session. Idempotent.
93
93
  fn close(&self) -> Result<(), Error> {
94
- let inner = Arc::clone(&self.inner);
94
+ let inner = Arc::clone(self.inner.get()?);
95
95
  let result: MicrosandboxResult<()> = block_on(async move {
96
96
  let client = {
97
97
  let mut guard = inner.lock().await;
@@ -122,7 +122,7 @@ fn ssh_output_to_hash(output: SshOutput) -> RHash {
122
122
 
123
123
  #[magnus::wrap(class = "Microsandbox::Native::SftpClient", free_immediately, size)]
124
124
  pub struct SftpClient {
125
- inner: Arc<Mutex<Option<CoreSftp>>>,
125
+ inner: ForkGuarded<Arc<Mutex<Option<CoreSftp>>>>,
126
126
  }
127
127
 
128
128
  /// Format an SFTP-layer error as a plain string inside `block_on`; the Ruby
@@ -134,12 +134,15 @@ fn sftp_str(e: impl std::fmt::Display) -> String {
134
134
  impl SftpClient {
135
135
  pub fn from_core(inner: CoreSftp) -> Self {
136
136
  Self {
137
- inner: Arc::new(Mutex::new(Some(inner))),
137
+ inner: ForkGuarded::new(
138
+ "Microsandbox::SftpClient",
139
+ Arc::new(Mutex::new(Some(inner))),
140
+ ),
138
141
  }
139
142
  }
140
143
 
141
144
  fn read(&self, path: String) -> Result<RString, Error> {
142
- let inner = Arc::clone(&self.inner);
145
+ let inner = Arc::clone(self.inner.get()?);
143
146
  let result: Result<Vec<u8>, String> = block_on(async move {
144
147
  let guard = inner.lock().await;
145
148
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -150,7 +153,7 @@ impl SftpClient {
150
153
 
151
154
  fn write(&self, path: String, data: RString) -> Result<(), Error> {
152
155
  let bytes = unsafe { data.as_slice() }.to_vec();
153
- let inner = Arc::clone(&self.inner);
156
+ let inner = Arc::clone(self.inner.get()?);
154
157
  let result: Result<(), String> = block_on(async move {
155
158
  let guard = inner.lock().await;
156
159
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -163,7 +166,7 @@ impl SftpClient {
163
166
  }
164
167
 
165
168
  fn mkdir(&self, path: String) -> Result<(), Error> {
166
- let inner = Arc::clone(&self.inner);
169
+ let inner = Arc::clone(self.inner.get()?);
167
170
  let result: Result<(), String> = block_on(async move {
168
171
  let guard = inner.lock().await;
169
172
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -173,7 +176,7 @@ impl SftpClient {
173
176
  }
174
177
 
175
178
  fn remove_file(&self, path: String) -> Result<(), Error> {
176
- let inner = Arc::clone(&self.inner);
179
+ let inner = Arc::clone(self.inner.get()?);
177
180
  let result: Result<(), String> = block_on(async move {
178
181
  let guard = inner.lock().await;
179
182
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -183,7 +186,7 @@ impl SftpClient {
183
186
  }
184
187
 
185
188
  fn remove_dir(&self, path: String) -> Result<(), Error> {
186
- let inner = Arc::clone(&self.inner);
189
+ let inner = Arc::clone(self.inner.get()?);
187
190
  let result: Result<(), String> = block_on(async move {
188
191
  let guard = inner.lock().await;
189
192
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -193,7 +196,7 @@ impl SftpClient {
193
196
  }
194
197
 
195
198
  fn rename(&self, old_path: String, new_path: String) -> Result<(), Error> {
196
- let inner = Arc::clone(&self.inner);
199
+ let inner = Arc::clone(self.inner.get()?);
197
200
  let result: Result<(), String> = block_on(async move {
198
201
  let guard = inner.lock().await;
199
202
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -203,7 +206,7 @@ impl SftpClient {
203
206
  }
204
207
 
205
208
  fn symlink(&self, target: String, link_path: String) -> Result<(), Error> {
206
- let inner = Arc::clone(&self.inner);
209
+ let inner = Arc::clone(self.inner.get()?);
207
210
  let result: Result<(), String> = block_on(async move {
208
211
  let guard = inner.lock().await;
209
212
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -213,7 +216,7 @@ impl SftpClient {
213
216
  }
214
217
 
215
218
  fn real_path(&self, path: String) -> Result<String, Error> {
216
- let inner = Arc::clone(&self.inner);
219
+ let inner = Arc::clone(self.inner.get()?);
217
220
  let result: Result<String, String> = block_on(async move {
218
221
  let guard = inner.lock().await;
219
222
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -223,7 +226,7 @@ impl SftpClient {
223
226
  }
224
227
 
225
228
  fn read_link(&self, path: String) -> Result<String, Error> {
226
- let inner = Arc::clone(&self.inner);
229
+ let inner = Arc::clone(self.inner.get()?);
227
230
  let result: Result<String, String> = block_on(async move {
228
231
  let guard = inner.lock().await;
229
232
  let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
@@ -233,7 +236,7 @@ impl SftpClient {
233
236
  }
234
237
 
235
238
  fn close(&self) -> Result<(), Error> {
236
- let inner = Arc::clone(&self.inner);
239
+ let inner = Arc::clone(self.inner.get()?);
237
240
  let result: Result<(), String> = block_on(async move {
238
241
  let sftp = {
239
242
  let mut guard = inner.lock().await;
@@ -254,19 +257,19 @@ impl SftpClient {
254
257
 
255
258
  #[magnus::wrap(class = "Microsandbox::Native::SshServer", free_immediately, size)]
256
259
  pub struct SshServer {
257
- inner: Arc<Mutex<Option<CoreSshServer>>>,
260
+ inner: ForkGuarded<Arc<Mutex<Option<CoreSshServer>>>>,
258
261
  }
259
262
 
260
263
  impl SshServer {
261
264
  pub fn from_core(inner: CoreSshServer) -> Self {
262
265
  Self {
263
- inner: Arc::new(Mutex::new(Some(inner))),
266
+ inner: ForkGuarded::new("Microsandbox::SshServer", Arc::new(Mutex::new(Some(inner)))),
264
267
  }
265
268
  }
266
269
 
267
270
  /// Serve one SSH connection over this process's stdin/stdout.
268
271
  fn serve_connection(&self) -> Result<(), Error> {
269
- let inner = Arc::clone(&self.inner);
272
+ let inner = Arc::clone(self.inner.get()?);
270
273
  let result: MicrosandboxResult<()> = block_on(async move {
271
274
  let server = {
272
275
  let guard = inner.lock().await;
@@ -282,7 +285,7 @@ impl SshServer {
282
285
 
283
286
  /// Release the prepared server endpoint. Idempotent.
284
287
  fn close(&self) -> Result<(), Error> {
285
- let inner = Arc::clone(&self.inner);
288
+ let inner = Arc::clone(self.inner.get()?);
286
289
  block_on(async move {
287
290
  inner.lock().await.take();
288
291
  })?;
@@ -18,14 +18,14 @@ use microsandbox::MicrosandboxResult;
18
18
  use tokio::sync::Mutex;
19
19
 
20
20
  use crate::error;
21
- use crate::runtime::block_on;
21
+ use crate::runtime::{block_on, ForkGuarded};
22
22
  use crate::sandbox::{log_entry_to_hash, metrics_to_hash};
23
23
 
24
24
  type BoxStream<T> = Pin<Box<dyn Stream<Item = MicrosandboxResult<T>> + Send>>;
25
25
 
26
26
  #[magnus::wrap(class = "Microsandbox::Native::LogStream", free_immediately, size)]
27
27
  pub struct LogStream {
28
- inner: Arc<Mutex<BoxStream<LogEntry>>>,
28
+ inner: ForkGuarded<Arc<Mutex<BoxStream<LogEntry>>>>,
29
29
  }
30
30
 
31
31
  impl LogStream {
@@ -33,14 +33,17 @@ impl LogStream {
33
33
  stream: impl Stream<Item = MicrosandboxResult<LogEntry>> + Send + 'static,
34
34
  ) -> Self {
35
35
  Self {
36
- inner: Arc::new(Mutex::new(Box::pin(stream))),
36
+ inner: ForkGuarded::new(
37
+ "Microsandbox::LogStream",
38
+ Arc::new(Mutex::new(Box::pin(stream))),
39
+ ),
37
40
  }
38
41
  }
39
42
 
40
43
  /// Next log entry as a Hash, or nil when the stream ends.
41
44
  fn recv(&self) -> Result<Option<RHash>, Error> {
42
45
  use futures::StreamExt;
43
- let inner = Arc::clone(&self.inner);
46
+ let inner = Arc::clone(self.inner.get()?);
44
47
  match block_on(async move { inner.lock().await.next().await })? {
45
48
  Some(Ok(entry)) => Ok(Some(log_entry_to_hash(&entry))),
46
49
  Some(Err(e)) => Err(error::to_ruby(e)),
@@ -51,7 +54,7 @@ impl LogStream {
51
54
 
52
55
  #[magnus::wrap(class = "Microsandbox::Native::MetricsStream", free_immediately, size)]
53
56
  pub struct MetricsStream {
54
- inner: Arc<Mutex<BoxStream<SandboxMetrics>>>,
57
+ inner: ForkGuarded<Arc<Mutex<BoxStream<SandboxMetrics>>>>,
55
58
  }
56
59
 
57
60
  impl MetricsStream {
@@ -59,14 +62,17 @@ impl MetricsStream {
59
62
  stream: impl Stream<Item = MicrosandboxResult<SandboxMetrics>> + Send + 'static,
60
63
  ) -> Self {
61
64
  Self {
62
- inner: Arc::new(Mutex::new(Box::pin(stream))),
65
+ inner: ForkGuarded::new(
66
+ "Microsandbox::MetricsStream",
67
+ Arc::new(Mutex::new(Box::pin(stream))),
68
+ ),
63
69
  }
64
70
  }
65
71
 
66
72
  /// Next metrics snapshot as a Hash, or nil when the stream ends.
67
73
  fn recv(&self) -> Result<Option<RHash>, Error> {
68
74
  use futures::StreamExt;
69
- let inner = Arc::clone(&self.inner);
75
+ let inner = Arc::clone(self.inner.get()?);
70
76
  match block_on(async move { inner.lock().await.next().await })? {
71
77
  Some(Ok(metrics)) => Ok(Some(metrics_to_hash(&metrics))),
72
78
  Some(Err(e)) => Err(error::to_ruby(e)),
@@ -12,7 +12,7 @@ use microsandbox::{Backend, MicrosandboxError};
12
12
 
13
13
  use crate::conv;
14
14
  use crate::error;
15
- use crate::runtime::{block_on, ruby};
15
+ use crate::runtime::{block_on, ruby, ForkGuarded};
16
16
  use crate::sandbox::{fs_entry_to_hash, fs_metadata_to_hash};
17
17
 
18
18
  fn handle_to_hash(h: &VolumeHandle) -> RHash {
@@ -128,7 +128,9 @@ fn contained(path: &str) -> Result<&str, Error> {
128
128
  /// each operation rebuilds it from the stored backend + name.
129
129
  #[magnus::wrap(class = "Microsandbox::Native::VolumeFs", free_immediately, size)]
130
130
  pub struct VolumeFs {
131
- backend: Arc<dyn Backend>,
131
+ /// The backend this view is bound to — a clone of the creating process's
132
+ /// default backend, so it must not cross a fork.
133
+ backend: ForkGuarded<Arc<dyn Backend>>,
132
134
  name: String,
133
135
  }
134
136
 
@@ -138,40 +140,44 @@ impl VolumeFs {
138
140
  /// trait, which yields precise per-operation `Unsupported` errors on
139
141
  /// backends that can't serve it — no local-only downcast needed here.
140
142
  fn for_volume(name: String) -> Result<VolumeFs, Error> {
143
+ crate::backend::ensure_this_process_backend()?;
141
144
  Ok(VolumeFs {
142
- backend: microsandbox::default_backend(),
145
+ backend: ForkGuarded::new("Microsandbox::VolumeFs", microsandbox::default_backend()),
143
146
  name,
144
147
  })
145
148
  }
146
149
 
147
- fn fs(&self) -> microsandbox::volume::VolumeFs<'_> {
148
- microsandbox::volume::VolumeFs::with_backend(self.backend.clone(), &self.name)
150
+ fn fs(&self) -> Result<microsandbox::volume::VolumeFs<'_>, Error> {
151
+ let backend = self.backend.get()?.clone();
152
+ Ok(microsandbox::volume::VolumeFs::with_backend(
153
+ backend, &self.name,
154
+ ))
149
155
  }
150
156
 
151
157
  fn read(&self, path: String) -> Result<RString, Error> {
158
+ let fs = self.fs()?;
152
159
  let path = contained(&path)?;
153
- let fs = self.fs();
154
160
  let bytes = block_on(fs.read(path))?.map_err(error::to_ruby)?;
155
161
  Ok(ruby().str_from_slice(bytes.as_ref()))
156
162
  }
157
163
 
158
164
  fn read_text(&self, path: String) -> Result<String, Error> {
165
+ let fs = self.fs()?;
159
166
  let path = contained(&path)?;
160
- let fs = self.fs();
161
167
  block_on(fs.read_to_string(path))?.map_err(error::to_ruby)
162
168
  }
163
169
 
164
170
  fn write(&self, path: String, data: RString) -> Result<(), Error> {
165
171
  // Copy the bytes out while the GVL is held (GC.compact could move them).
166
172
  let bytes = unsafe { data.as_slice() }.to_vec();
173
+ let fs = self.fs()?;
167
174
  let path = contained(&path)?;
168
- let fs = self.fs();
169
175
  block_on(fs.write(path, &bytes))?.map_err(error::to_ruby)
170
176
  }
171
177
 
172
178
  fn list(&self, path: String) -> Result<RArray, Error> {
179
+ let fs = self.fs()?;
173
180
  let path = contained(&path)?;
174
- let fs = self.fs();
175
181
  let entries = block_on(fs.list(path))?.map_err(error::to_ruby)?;
176
182
  let arr = ruby().ary_new();
177
183
  for entry in &entries {
@@ -181,46 +187,46 @@ impl VolumeFs {
181
187
  }
182
188
 
183
189
  fn mkdir(&self, path: String) -> Result<(), Error> {
190
+ let fs = self.fs()?;
184
191
  let path = contained(&path)?;
185
- let fs = self.fs();
186
192
  block_on(fs.mkdir(path))?.map_err(error::to_ruby)
187
193
  }
188
194
 
189
195
  fn remove_file(&self, path: String) -> Result<(), Error> {
196
+ let fs = self.fs()?;
190
197
  let path = contained(&path)?;
191
- let fs = self.fs();
192
198
  block_on(fs.remove(path))?.map_err(error::to_ruby)
193
199
  }
194
200
 
195
201
  fn remove_dir(&self, path: String) -> Result<(), Error> {
202
+ let fs = self.fs()?;
196
203
  let path = contained(&path)?;
197
- let fs = self.fs();
198
204
  block_on(fs.remove_dir(path))?.map_err(error::to_ruby)
199
205
  }
200
206
 
201
207
  fn exists(&self, path: String) -> Result<bool, Error> {
208
+ let fs = self.fs()?;
202
209
  let path = contained(&path)?;
203
- let fs = self.fs();
204
210
  block_on(fs.exists(path))?.map_err(error::to_ruby)
205
211
  }
206
212
 
207
213
  fn copy(&self, from: String, to: String) -> Result<(), Error> {
214
+ let fs = self.fs()?;
208
215
  let from = contained(&from)?;
209
216
  let to = contained(&to)?;
210
- let fs = self.fs();
211
217
  block_on(fs.copy(from, to))?.map_err(error::to_ruby)
212
218
  }
213
219
 
214
220
  fn rename(&self, from: String, to: String) -> Result<(), Error> {
221
+ let fs = self.fs()?;
215
222
  let from = contained(&from)?;
216
223
  let to = contained(&to)?;
217
- let fs = self.fs();
218
224
  block_on(fs.rename(from, to))?.map_err(error::to_ruby)
219
225
  }
220
226
 
221
227
  fn stat(&self, path: String) -> Result<RHash, Error> {
228
+ let fs = self.fs()?;
222
229
  let path = contained(&path)?;
223
- let fs = self.fs();
224
230
  let meta = block_on(fs.stat(path))?.map_err(error::to_ruby)?;
225
231
  Ok(fs_metadata_to_hash(&meta))
226
232
  }
@@ -102,6 +102,14 @@ module Microsandbox
102
102
  define_error(:CloudHttpError, "cloud-http")
103
103
  define_error(:UnsupportedError, "unsupported")
104
104
 
105
+ # Process model ------------------------------------------------------------
106
+ # A handle (a Sandbox, a SandboxHandle, an ExecHandle, a stream, an SSH or
107
+ # agent client, a VolumeFs, ...) was used in a process forked from the one
108
+ # that created it. Handles are bound to their creating process; a forked
109
+ # child opens its own (`Sandbox.get(name).connect`, `Volume.fs(name)`, ...).
110
+ # The message names both pids. Ruby-only: no other SDK has a counterpart.
111
+ define_error(:ForkedHandleError, "forked-handle")
112
+
105
113
  # As of runtime v0.6.8 the core keys Unsupported errors by a structured
106
114
  # (operation, reason) pair. The native layer renders both into the message
107
115
  # ("sandbox.kill is not supported by this backend: ...") and also attaches
@@ -8,7 +8,7 @@ module Microsandbox
8
8
  # Versioning section of the README for the full gem-to-runtime map. Must equal
9
9
  # the native ext's Cargo crate version (`Native.version`), enforced by
10
10
  # spec/unit/version_spec.rb.
11
- VERSION = "0.17.2"
11
+ VERSION = "0.17.3"
12
12
 
13
13
  # The upstream microsandbox runtime release this gem build embeds — the `tag`
14
14
  # pinned on the `microsandbox`/`microsandbox-network` git deps in
data/lib/microsandbox.rb CHANGED
@@ -240,6 +240,15 @@ module Microsandbox
240
240
  # avoid calling {set_default_backend} inside the block (the restore on exit
241
241
  # would overwrite that change). Mirrors the official SDKs' scoped-backend helper.
242
242
  #
243
+ # The restore can fail in one situation: the previous backend belonged to a
244
+ # parent process (the block is left in a child forked while it was running,
245
+ # or it is a forked child's first use of the SDK) and cannot be chosen
246
+ # again there — it came from the environment or config file, which no
247
+ # longer resolve. The block's temporary selection then stays in effect. If
248
+ # the block itself raised, its exception is the one that propagates and the
249
+ # restore failure is only warned about; otherwise the restore failure is
250
+ # raised.
251
+ #
243
252
  # @param kind ["local","cloud", Symbol]
244
253
  # @param url [String, nil]
245
254
  # @param api_key [String, nil]
@@ -248,10 +257,20 @@ module Microsandbox
248
257
  # @return [Object] the block's return value
249
258
  def with_backend(kind, url: nil, api_key: nil, profile: nil)
250
259
  token = Native.push_default_backend(kind.to_s, url&.to_s, api_key&.to_s, profile&.to_s)
260
+ raised = false
251
261
  begin
252
262
  yield
263
+ rescue Exception # standard:disable Lint/RescueException -- only noted, re-raised at once
264
+ raised = true
265
+ raise
253
266
  ensure
254
- Native.pop_default_backend(token)
267
+ begin
268
+ Native.pop_default_backend(token)
269
+ rescue Microsandbox::Error => e
270
+ # Never let the restore failure replace what the block raised.
271
+ raise unless raised
272
+ warn "microsandbox: with_backend could not restore the previous backend: #{e.message}"
273
+ end
255
274
  end
256
275
  end
257
276
 
data/sig/microsandbox.rbs CHANGED
@@ -64,6 +64,7 @@ module Microsandbox
64
64
  class MetricsUnavailableError < Error end
65
65
  class UnsupportedOperationError < Error end
66
66
  class CloudHttpError < Error end
67
+ class ForkedHandleError < Error end
67
68
  class UnsupportedError < Error
68
69
  def operation: () -> String?
69
70
  def hint: () -> String?
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.2
4
+ version: 0.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao