microsandbox-rb 0.15.0 → 0.17.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 +4 -4
- data/CHANGELOG.md +197 -0
- data/Cargo.lock +219 -171
- data/DESIGN.md +1 -1
- data/README.md +83 -4
- data/ext/microsandbox/Cargo.toml +5 -5
- data/ext/microsandbox/src/error.rs +12 -0
- data/ext/microsandbox/src/sandbox.rs +264 -16
- data/lib/microsandbox/errors.rb +7 -0
- data/lib/microsandbox/outbound_proxy.rb +332 -0
- data/lib/microsandbox/sandbox.rb +316 -3
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox.rb +1 -0
- data/sig/microsandbox.rbs +56 -1
- metadata +2 -1
data/DESIGN.md
CHANGED
|
@@ -184,7 +184,7 @@ extension.
|
|
|
184
184
|
|
|
185
185
|
`ext/microsandbox/Cargo.toml` depends on the core crate via a **pinned git tag**
|
|
186
186
|
(`microsandbox` / `microsandbox-network`, pinned to the same tag as
|
|
187
|
-
`Microsandbox::RUNTIME_VERSION` — currently `v0.6.
|
|
187
|
+
`Microsandbox::RUNTIME_VERSION` — currently `v0.6.18`), so the gem builds anywhere
|
|
188
188
|
— CI, `rake-compiler-dock` release containers, and end-user source installs —
|
|
189
189
|
without an adjacent checkout. For fast local development against a sibling
|
|
190
190
|
microsandbox checkout, copy `.cargo/config.toml.example` to `.cargo/config.toml`
|
data/README.md
CHANGED
|
@@ -186,6 +186,39 @@ Microsandbox::Sandbox.start("box") # restart a stopped sandbox
|
|
|
186
186
|
Microsandbox::Sandbox.remove("box") # remove a stopped sandbox
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
**Convergent lifecycle** (runtime `v0.6.16`) — idempotent operations that take a
|
|
190
|
+
name to the state you want, whatever state it is in now:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# Create it, or connect to (and start) the one that is already there. The
|
|
194
|
+
# keyword options are `create`'s and apply only when a create actually happens.
|
|
195
|
+
sb = Microsandbox::Sandbox.connect_or_create("box", image: "public.ecr.aws/docker/library/alpine:latest")
|
|
196
|
+
|
|
197
|
+
sb.id # opaque identity of the *persisted* sandbox — unlike
|
|
198
|
+
# the reusable name, it changes on remove+recreate
|
|
199
|
+
sb.wait_for_status(:running) # => SandboxHandle (no built-in timeout)
|
|
200
|
+
sb = sb.restart # stop + start; => a new live Sandbox
|
|
201
|
+
sb.destroy # stop + remove, in one step
|
|
202
|
+
|
|
203
|
+
h = Microsandbox::Sandbox.get("box")
|
|
204
|
+
h.connect_or_start # connect if running, start if not => Sandbox
|
|
205
|
+
h.restart(force: true, timeout: 5)
|
|
206
|
+
h.destroy
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Everything that acts on an *existing* receiver — `wait_for_status`, `restart`,
|
|
210
|
+
`destroy`, `connect_or_start` — compares the identity it was bound to against
|
|
211
|
+
the name's current owner first, raising `Microsandbox::SandboxReplacedError`
|
|
212
|
+
rather than touching a sandbox someone else recreated under the same name.
|
|
213
|
+
(`connect_or_create` is the entry point, so it has no prior identity to check:
|
|
214
|
+
it simply converges on whichever sandbox now owns the name.)
|
|
215
|
+
|
|
216
|
+
`connect_or_create`'s block form stops the sandbox only when the call owns its
|
|
217
|
+
lifecycle — i.e. when it created it attached. A sandbox it merely connected to,
|
|
218
|
+
or started with `detached: true`, is left running. And the stop it does issue is
|
|
219
|
+
scoped to that sandbox's `id`, so a name removed and recreated while the block
|
|
220
|
+
ran cannot be taken down by the teardown either.
|
|
221
|
+
|
|
189
222
|
> **v0.5.8 lifecycle change.** Upstream split the lifecycle into the live
|
|
190
223
|
> `Sandbox` and a controllable `SandboxHandle`, and the gem mirrors it. The live
|
|
191
224
|
> `Sandbox#stop`/`#kill` no longer take a `timeout:`; `#request_stop`/
|
|
@@ -212,6 +245,47 @@ Microsandbox::Sandbox.create(
|
|
|
212
245
|
end
|
|
213
246
|
```
|
|
214
247
|
|
|
248
|
+
**Outbound proxy** (runtime `v0.6.17`) — route the sandbox's egress through a
|
|
249
|
+
SOCKS4 (TCP) or SOCKS5 (TCP + non-DNS UDP) proxy with `proxy:`. The proxy is
|
|
250
|
+
dialed by the runtime's host-side network stack, so its address is resolved
|
|
251
|
+
from the host (`127.0.0.1` is the host's loopback), and the egress policy
|
|
252
|
+
(`network:`) still governs which destinations may be reached. A SOCKS5 password
|
|
253
|
+
comes from a host environment variable via `SecretSource.env` — only the
|
|
254
|
+
variable's *name* is handed to the runtime. Local backend only.
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
Microsandbox::Sandbox.create("worker", image: "python",
|
|
258
|
+
proxy: Microsandbox::OutboundProxy.socks5("127.0.0.1:1080"))
|
|
259
|
+
|
|
260
|
+
Microsandbox::Sandbox.create("worker", image: "python",
|
|
261
|
+
proxy: Microsandbox::OutboundProxy.socks5("10.0.0.5:1080")
|
|
262
|
+
.credentials("sandbox", Microsandbox::SecretSource.env("PROXY_PASSWORD")))
|
|
263
|
+
|
|
264
|
+
Microsandbox::Sandbox.create("worker", image: "python",
|
|
265
|
+
proxy: Microsandbox::OutboundProxy.socks4("127.0.0.1:1080", user_id: "ci"))
|
|
266
|
+
|
|
267
|
+
# The equivalent plain Hash works too:
|
|
268
|
+
Microsandbox::Sandbox.create("worker", image: "python",
|
|
269
|
+
proxy: { protocol: :socks5, address: "10.0.0.5:1080",
|
|
270
|
+
credentials: { username: "sandbox", password: { env: "PROXY_PASSWORD" } } })
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Strict hostname policy** (runtime `v0.6.18`) — `strict: true` makes a
|
|
274
|
+
hostname-rule allow fail closed unless the runtime can actually see the request
|
|
275
|
+
authority (plain-HTTP `Host`, or SNI/`:authority` under TLS interception); a
|
|
276
|
+
bypassed or non-intercepted HTTPS flow allowed only by a hostname rule is then
|
|
277
|
+
denied before the upstream dial. Default `false`; create-only. Independently of
|
|
278
|
+
`strict:`, any policy with domain rules now checks plain-HTTP `Host` headers
|
|
279
|
+
against it.
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
Microsandbox::Sandbox.create("worker", image: "python",
|
|
283
|
+
network: Microsandbox::NetworkPolicy.custom(default_egress: :deny,
|
|
284
|
+
rules: [{ action: :allow, direction: :egress, protocol: :tcp, port: 443,
|
|
285
|
+
destination: Microsandbox::Destination.domain("api.example.com") }]),
|
|
286
|
+
strict: true)
|
|
287
|
+
```
|
|
288
|
+
|
|
215
289
|
### Executing commands
|
|
216
290
|
|
|
217
291
|
```ruby
|
|
@@ -430,7 +504,9 @@ Microsandbox::Volume.remove("cache")
|
|
|
430
504
|
```
|
|
431
505
|
|
|
432
506
|
`volumes:` accepts a host path String (bind mount) or `{ bind: "/host" }` /
|
|
433
|
-
`{ named: "volume-name" }` per guest path.
|
|
507
|
+
`{ named: "volume-name" }` per guest path. A bind or named mount may pin the
|
|
508
|
+
fallback guest owner for host files with `uid:`/`gid:` (both required together,
|
|
509
|
+
runtime `v0.6.15`). Boot from a snapshot with
|
|
434
510
|
`Sandbox.create(name, from_snapshot: "snap-name-or-path")`.
|
|
435
511
|
|
|
436
512
|
### Error handling
|
|
@@ -524,8 +600,8 @@ change diverged the two numbers — the gem version is **not** a reliable indica
|
|
|
524
600
|
of the embedded runtime version. To learn which runtime a build wraps, ask it:
|
|
525
601
|
|
|
526
602
|
```ruby
|
|
527
|
-
Microsandbox::VERSION # => "0.
|
|
528
|
-
Microsandbox.runtime_version # => "v0.6.
|
|
603
|
+
Microsandbox::VERSION # => "0.17.0" (the gem's own version)
|
|
604
|
+
Microsandbox.runtime_version # => "v0.6.18" (the embedded upstream runtime tag)
|
|
529
605
|
```
|
|
530
606
|
|
|
531
607
|
The companion [`microsandbox-rb-binaries`](#the-runtime-binaries) gem is
|
|
@@ -558,6 +634,8 @@ stale.
|
|
|
558
634
|
| `0.13.0` | `v0.6.9` | adopts upstream `v0.6.9` (**breaking**): a bare `MSB_API_KEY` no longer selects the cloud backend (explicit `MSB_BACKEND=cloud` or a cloud profile required; invalid cloud config fails closed with `InvalidConfigError`); snapshot payload integrity becomes opt-in (`record_integrity:`, `verify` can report `:not_recorded`). Parity: default-workload execution (`exec_default`/`exec_default_stream`/`attach_default`, `cmd:`), flat root disks (`RootDisk.flat`), `modify(root_disk_size:)`, `rate_limiter:`, `vsock:`, `default_backend_info`, `Volume.get_default` |
|
|
559
635
|
| `0.14.0` | `v0.6.9` | two-gem split: SDK-only gem (no build-time runtime download) + companion `microsandbox-rb-binaries` platform gems |
|
|
560
636
|
| `0.15.0` | `v0.6.14` | adopts upstream `v0.6.10`–`v0.6.14` step by step: bind-mount correctness, guest bootstrap off the kernel command line, DNS pins for deferred domain allows, Linux glibc 2.28 baseline for the prebuilt runtime, legacy ext4 upper-disk resize, `msb_krun` 0.1.32. Parity: `ssh.open_client`/`prepare_server` accept `inactivity_timeout:` (seconds; `0` disables, `nil` inherits the 600s global default) |
|
|
637
|
+
| `0.16.0` | `v0.6.16` | adopts upstream `v0.6.15`+`v0.6.16` step by step: mount fallback ownership, readonly-mount write-probe fix, log retrieval rerouted through the SDK backends, config overlaid by field presence, network-slot recycling. Parity: per-mount `uid:`/`gid:`, and the convergent lifecycle — `Sandbox.connect_or_create`, `#id`, `#wait_for_status`, `#restart`, `#destroy`, `SandboxHandle#connect_or_start`, `SandboxReplacedError` |
|
|
638
|
+
| `0.17.0` | `v0.6.18` | adopts upstream `v0.6.17`+`v0.6.18` step by step: outbound SOCKS4/SOCKS5 proxies (SOCKS5 UDP + credentials), migration-order fix for databases last opened by a `v0.6.15` `msb`, security hardening — plain-HTTP `Host`/`:authority` now checked against domain-rule policies even in non-strict mode, plus fail-closed strict hostname mode. Parity: `proxy:` (`Microsandbox::OutboundProxy` / `SecretSource`), `strict:` |
|
|
561
639
|
|
|
562
640
|
**Going forward** — the gem version moves on its own semver track and no longer
|
|
563
641
|
mirrors the upstream tag:
|
|
@@ -672,7 +750,8 @@ image-pull progress (`Sandbox.create_with_progress` → `PullSession`),
|
|
|
672
750
|
(`root_disk:` managed/tmpfs/disk via `Microsandbox::RootDisk`), **network
|
|
673
751
|
configuration** (composable profiles, custom per-rule
|
|
674
752
|
`Microsandbox::NetworkPolicy`/`Rule`/`Destination`, plus DNS, TLS interception,
|
|
675
|
-
IPv4/IPv6 pools, `max_connections`, `trust_host_cas`
|
|
753
|
+
IPv4/IPv6 pools, `max_connections`, `trust_host_cas`, `strict` hostname mode,
|
|
754
|
+
outbound SOCKS4/SOCKS5 `proxy:`), **secrets** (multi-host /
|
|
676
755
|
wildcard allow-lists, injection toggles, per-secret + sandbox-level violation
|
|
677
756
|
policy), **SSH** (`Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`), and the
|
|
678
757
|
**raw agent client** (`Microsandbox::AgentClient`). Create options span
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -6,8 +6,8 @@ name = "microsandbox_rb"
|
|
|
6
6
|
description = "Ruby SDK native extension for microsandbox — secure, fast microVM-based sandboxing."
|
|
7
7
|
# Must equal Microsandbox::VERSION (lib/microsandbox/version.rb) — Native.version
|
|
8
8
|
# returns this via env!("CARGO_PKG_VERSION") and version_spec.rb asserts equality.
|
|
9
|
-
# The core-crate dependency below stays pinned at its own tag (v0.6.
|
|
10
|
-
version = "0.
|
|
9
|
+
# The core-crate dependency below stays pinned at its own tag (v0.6.16).
|
|
10
|
+
version = "0.17.0"
|
|
11
11
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
12
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
13
|
license = "Apache-2.0"
|
|
@@ -42,8 +42,8 @@ rb-sys = "0.9"
|
|
|
42
42
|
# first-use download into ~/.microsandbox (`setup::install`, which is not
|
|
43
43
|
# gated on "prebuilt"). "net"/"ssh"/"keyring" are the features the official
|
|
44
44
|
# SDKs ship with.
|
|
45
|
-
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.
|
|
46
|
-
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.
|
|
45
|
+
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.18", default-features = false, features = ["keyring", "net", "ssh"] }
|
|
46
|
+
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.18" }
|
|
47
47
|
# The *guest* agent (`agentd`, PID 1 inside every microVM) is embedded into the
|
|
48
48
|
# extension by microsandbox-filesystem's build.rs, which without its "prebuilt"
|
|
49
49
|
# feature demands a locally built `build/agentd` (workspace-only, `just
|
|
@@ -52,7 +52,7 @@ microsandbox-network = { git = "https://github.com/superradcompany/microsandbox"
|
|
|
52
52
|
# prebuilt agentd for the target arch is fetched (same behaviour as before)
|
|
53
53
|
# while the SDK-level msb download above stays off. Feature unification means
|
|
54
54
|
# this adds no new crates; keep it on the same tag as the deps above.
|
|
55
|
-
microsandbox-runtime = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.
|
|
55
|
+
microsandbox-runtime = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.18", default-features = false, features = ["prebuilt"] }
|
|
56
56
|
|
|
57
57
|
# Async core bridged to Ruby's synchronous API via a blocking tokio runtime.
|
|
58
58
|
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
use magnus::{value::ReprValue, Error, ExceptionClass, Module, RClass, RModule, Ruby};
|
|
9
9
|
use microsandbox::{AgentClientError, MicrosandboxError, Operation, UnsupportedReason};
|
|
10
|
+
use microsandbox_network::policy::BuildError;
|
|
10
11
|
|
|
11
12
|
/// The Ruby class (relative to the `Microsandbox` module) for a core error.
|
|
12
13
|
/// `"Error"` is the base class; anything else is a named subclass.
|
|
@@ -17,6 +18,11 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
17
18
|
SandboxNotFound(_) => "SandboxNotFoundError",
|
|
18
19
|
SandboxAlreadyExists(_) => "SandboxAlreadyExistsError",
|
|
19
20
|
SandboxStillRunning(_) => "SandboxStillRunningError",
|
|
21
|
+
// v0.6.16 (#1462): the receiver's captured identity no longer owns the
|
|
22
|
+
// name — it was removed and recreated. Raised by the identity-checked
|
|
23
|
+
// convergent lifecycle APIs instead of acting on the replacement.
|
|
24
|
+
// Mirrors the Python `SandboxReplacedError`.
|
|
25
|
+
SandboxReplaced { .. } => "SandboxReplacedError",
|
|
20
26
|
// v0.6.6 (#1099): the sandbox exists but isn't running. Raised by the
|
|
21
27
|
// handle's exec/attach/ping/touch not-running guards and the fs
|
|
22
28
|
// agent-endpoint lookup. The `SandboxNotRunningError` class already
|
|
@@ -55,6 +61,12 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
|
55
61
|
// migration (run at backend connect / artifact open) failed and needs
|
|
56
62
|
// repair. Mirrors the Python `SnapshotMigrationError`.
|
|
57
63
|
SnapshotMigration { .. } => "SnapshotMigrationError",
|
|
64
|
+
// v0.6.17: a malformed `proxy:` (unparseable `IP:port`, invalid SOCKS4
|
|
65
|
+
// user ID, ...) also arrives as a `NetworkBuilder` error, but it is a
|
|
66
|
+
// *configuration* mistake, not a policy one — route it to
|
|
67
|
+
// `InvalidConfigError` like every other bad create option. Matched on
|
|
68
|
+
// the variant, never on the message text.
|
|
69
|
+
NetworkBuilder(BuildError::InvalidOutboundProxy { .. }) => "InvalidConfigError",
|
|
58
70
|
// Give the already-defined-but-orphaned `NetworkPolicyError` a mapping:
|
|
59
71
|
// a builder parse/validation error from `network(|n| ...)`. The gem
|
|
60
72
|
// unconditionally enables the core's `net` feature (default-features),
|
|
@@ -18,11 +18,11 @@ use microsandbox::logs::{
|
|
|
18
18
|
LogCursor, LogEntry, LogOptions, LogSource, LogStreamOptions, LogStreamStart,
|
|
19
19
|
};
|
|
20
20
|
use microsandbox::sandbox::{
|
|
21
|
-
AttachOptionsBuilder, DiskImageFormat, EnvVar, FlatClone, FsEntry, FsEntryKind,
|
|
22
|
-
HostPermissions, Patch, PullPolicy, PullProgress, PullProgressHandle,
|
|
23
|
-
RootDiskBuilder, SandboxBuilder, SandboxHandle, SandboxMetrics,
|
|
24
|
-
SandboxModificationPatch, SandboxStatus, SandboxStopResult,
|
|
25
|
-
SecretModificationPatch, SecretSource, SecurityProfile, StatVirtualization,
|
|
21
|
+
AttachOptionsBuilder, DestroyOptions, DiskImageFormat, EnvVar, FlatClone, FsEntry, FsEntryKind,
|
|
22
|
+
FsMetadata, HostPermissions, Patch, PullPolicy, PullProgress, PullProgressHandle,
|
|
23
|
+
RestartOptions, RlimitResource, RootDiskBuilder, SandboxBuilder, SandboxHandle, SandboxMetrics,
|
|
24
|
+
SandboxModificationBuilder, SandboxModificationPatch, SandboxStatus, SandboxStopResult,
|
|
25
|
+
SecretBuilder, SecretModificationPatch, SecretSource, SecurityProfile, StatVirtualization,
|
|
26
26
|
};
|
|
27
27
|
use microsandbox::LogLevel;
|
|
28
28
|
use microsandbox::MicrosandboxResult;
|
|
@@ -167,7 +167,8 @@ impl Sandbox {
|
|
|
167
167
|
// Hash — guest (req), kind ("bind"/"named"/"tmpfs"/"disk"), source
|
|
168
168
|
// (bind/named/disk), size_mib (tmpfs/disk), format + fstype (disk),
|
|
169
169
|
// readonly/noexec/nosuid/nodev (bool), stat_virtualization,
|
|
170
|
-
// host_permissions
|
|
170
|
+
// host_permissions, override_uid/override_gid (u32 pair).
|
|
171
|
+
// Enum-valued options are validated up front (the
|
|
171
172
|
// volume closure can't return an error); the core validates the rest
|
|
172
173
|
// (e.g. rejecting stat_virtualization on tmpfs/disk) at create().
|
|
173
174
|
for m in conv::opt_hash_vec(opts, "volumes")? {
|
|
@@ -195,6 +196,14 @@ impl Sandbox {
|
|
|
195
196
|
let host_perms = conv::opt_string(m, "host_permissions")?
|
|
196
197
|
.map(|s| host_permissions_from_str(&s))
|
|
197
198
|
.transpose()?;
|
|
199
|
+
// v0.6.15: the fallback guest owner presented for host files that
|
|
200
|
+
// carry no per-file stat override. The Ruby layer validates the pair
|
|
201
|
+
// (both-or-neither, u32 range, bind/named only, not with
|
|
202
|
+
// stat_virtualization=off); whether a *named* volume is directory-
|
|
203
|
+
// or disk-backed is known only to the core, which rejects the
|
|
204
|
+
// disk-backed case at build().
|
|
205
|
+
let override_uid = conv::opt_u32(m, "override_uid")?;
|
|
206
|
+
let override_gid = conv::opt_u32(m, "override_gid")?;
|
|
198
207
|
// v0.6.7: mount-root symlink protection is on by default; this is
|
|
199
208
|
// the per-mount opt-out. Valid for bind/named-directory mounts —
|
|
200
209
|
// the core rejects it elsewhere at build().
|
|
@@ -251,6 +260,9 @@ impl Sandbox {
|
|
|
251
260
|
if let Some(hp) = host_perms {
|
|
252
261
|
mb = mb.host_permissions(hp);
|
|
253
262
|
}
|
|
263
|
+
if let (Some(uid), Some(gid)) = (override_uid, override_gid) {
|
|
264
|
+
mb = mb.owner(uid, gid);
|
|
265
|
+
}
|
|
254
266
|
if let Some(follow) = follow_root_symlinks {
|
|
255
267
|
mb = mb.follow_root_symlinks(follow);
|
|
256
268
|
}
|
|
@@ -378,12 +390,22 @@ impl Sandbox {
|
|
|
378
390
|
.transpose()?;
|
|
379
391
|
let max_connections = conv::opt::<usize>(opts, "max_connections")?;
|
|
380
392
|
let trust_host_cas = conv::opt::<bool>(opts, "trust_host_cas")?;
|
|
393
|
+
// strict: fail-closed hostname-policy enforcement (v0.6.18). A
|
|
394
|
+
// hostname-rule allow must be backed by an inspectable request
|
|
395
|
+
// authority (plain-HTTP Host, or SNI/authority under TLS interception);
|
|
396
|
+
// otherwise the flow is denied before the upstream dial. Mirrors the
|
|
397
|
+
// Python SDK's `Network(strict=...)`. Create-only here to match the
|
|
398
|
+
// Python SDK's `modify` surface, which does not expose `strict`; the
|
|
399
|
+
// core's generated network config patch (ConfigPatch derive) does
|
|
400
|
+
// carry a `strict` field, so this is SDK parity, not a core limit.
|
|
401
|
+
let strict = conv::opt::<bool>(opts, "strict")?;
|
|
381
402
|
if dns.is_some()
|
|
382
403
|
|| tls.is_some()
|
|
383
404
|
|| ipv4_pool.is_some()
|
|
384
405
|
|| ipv6_pool.is_some()
|
|
385
406
|
|| max_connections.is_some()
|
|
386
407
|
|| trust_host_cas.is_some()
|
|
408
|
+
|| strict.is_some()
|
|
387
409
|
{
|
|
388
410
|
b = b.network(move |mut n| {
|
|
389
411
|
if let Some(dns) = dns {
|
|
@@ -438,6 +460,9 @@ impl Sandbox {
|
|
|
438
460
|
if let Some(t) = trust_host_cas {
|
|
439
461
|
n = n.trust_host_cas(t);
|
|
440
462
|
}
|
|
463
|
+
if let Some(s) = strict {
|
|
464
|
+
n = n.strict(s);
|
|
465
|
+
}
|
|
441
466
|
n
|
|
442
467
|
});
|
|
443
468
|
}
|
|
@@ -463,6 +488,18 @@ impl Sandbox {
|
|
|
463
488
|
})
|
|
464
489
|
});
|
|
465
490
|
}
|
|
491
|
+
// proxy: outbound SOCKS4/SOCKS5 proxy for egress traffic (v0.6.17,
|
|
492
|
+
// upstream #1234/#1507). The Ruby layer (`OutboundProxy.coerce`) has
|
|
493
|
+
// already validated the shape and normalized it to the Python SDK's
|
|
494
|
+
// `_to_dict()` wire form: {protocol:, address:, user_id?:,
|
|
495
|
+
// credentials?: {username:, password: {kind: "env", var:}}}. Only the
|
|
496
|
+
// password's env var NAME travels; the core resolves it host-side.
|
|
497
|
+
// Applied exactly as sdk/python/src/helpers.rs does; the address is
|
|
498
|
+
// parsed by the core's proxy builder (an invalid one surfaces as
|
|
499
|
+
// NetworkBuilder(InvalidOutboundProxy) → InvalidConfigError, see error.rs).
|
|
500
|
+
if let Some(proxy) = conv::opt::<RHash>(opts, "proxy")? {
|
|
501
|
+
b = apply_outbound_proxy(b, proxy)?;
|
|
502
|
+
}
|
|
466
503
|
// init: hand guest PID 1 to an init system. The Ruby layer normalizes
|
|
467
504
|
// `init:` to a Hash { cmd:, args?:, env?: }. `init_with` with empty
|
|
468
505
|
// args/env builds the same HandoffInit as the plain `init(cmd)`, so route
|
|
@@ -509,6 +546,16 @@ impl Sandbox {
|
|
|
509
546
|
Ok(PullSession::new(handle, join))
|
|
510
547
|
}
|
|
511
548
|
|
|
549
|
+
/// Converge on a live sandbox with this name: connect to (or start) the
|
|
550
|
+
/// persisted one when it already exists, otherwise create it from `opts`.
|
|
551
|
+
/// Concurrent callers converge on the winning identity. The core rejects
|
|
552
|
+
/// the combination with `replace`.
|
|
553
|
+
fn connect_or_create(name: String, opts: RHash) -> Result<Sandbox, Error> {
|
|
554
|
+
let b = Self::build_builder(name, opts)?;
|
|
555
|
+
let inner = block_on(b.connect_or_create()).map_err(error::to_ruby)?;
|
|
556
|
+
Ok(Sandbox::from_inner(inner))
|
|
557
|
+
}
|
|
558
|
+
|
|
512
559
|
/// Restart a previously-defined sandbox by name.
|
|
513
560
|
fn start(name: String, opts: RHash) -> Result<Sandbox, Error> {
|
|
514
561
|
let detached = conv::opt_bool(opts, "detached")?;
|
|
@@ -580,6 +627,13 @@ impl Sandbox {
|
|
|
580
627
|
self.inner.name().to_string()
|
|
581
628
|
}
|
|
582
629
|
|
|
630
|
+
/// The opaque, backend-assigned identity of this persisted sandbox
|
|
631
|
+
/// (v0.6.16). Stable for the sandbox's lifetime, and different once the
|
|
632
|
+
/// same name is removed and recreated.
|
|
633
|
+
fn id(&self) -> String {
|
|
634
|
+
self.inner.id().to_string()
|
|
635
|
+
}
|
|
636
|
+
|
|
583
637
|
/// Run a command (no shell). `args` is an Array of strings; `opts` is a
|
|
584
638
|
/// string-keyed Hash (cwd, user, env, timeout, tty, stdin).
|
|
585
639
|
fn exec(&self, cmd: String, args: Vec<String>, opts: RHash) -> Result<RHash, Error> {
|
|
@@ -644,17 +698,23 @@ impl Sandbox {
|
|
|
644
698
|
Ok(ExecHandle::from_core(handle))
|
|
645
699
|
}
|
|
646
700
|
|
|
647
|
-
/// Graceful stop
|
|
648
|
-
///
|
|
649
|
-
///
|
|
650
|
-
///
|
|
701
|
+
/// Graceful stop (SIGTERM→SIGKILL escalation with a 10s default), routed
|
|
702
|
+
/// straight through the live sandbox like every other lifecycle method here
|
|
703
|
+
/// and like both official bindings (`sdk/python/src/sandbox.rs`,
|
|
704
|
+
/// `sdk/ruby/ext/.../lib.rs`).
|
|
705
|
+
///
|
|
706
|
+
/// This deliberately does NOT re-fetch a `SandboxHandle` by name first. It
|
|
707
|
+
/// used to, back when the core's live `stop` was not identity-scoped; as of
|
|
708
|
+
/// v0.6.16 the core routes `stop` → `request_stop` → `stop_identified(name,
|
|
709
|
+
/// self.identity())`, so going through `self.inner` is what makes the stop
|
|
710
|
+
/// refuse a same-name replacement (`SandboxReplacedError`) instead of
|
|
711
|
+
/// terminating whatever sandbox happens to own the name right now. The old
|
|
712
|
+
/// by-name refetch threw that identity away.
|
|
713
|
+
///
|
|
714
|
+
/// Fine-grained control — a custom timeout or fire-and-return `request_*` —
|
|
715
|
+
/// lives on `SandboxHandle`, obtained via `Sandbox.get`.
|
|
651
716
|
fn stop(&self) -> Result<(), Error> {
|
|
652
|
-
|
|
653
|
-
block_on(async move {
|
|
654
|
-
let handle = microsandbox::sandbox::Sandbox::get(&name).await?;
|
|
655
|
-
handle.stop().await
|
|
656
|
-
})
|
|
657
|
-
.map_err(error::to_ruby)
|
|
717
|
+
block_on(self.inner.stop()).map_err(error::to_ruby)
|
|
658
718
|
}
|
|
659
719
|
|
|
660
720
|
/// Graceful stop, then wait for the process to exit. Returns an exit-status
|
|
@@ -680,6 +740,29 @@ impl Sandbox {
|
|
|
680
740
|
Ok(exit_status_to_hash(status))
|
|
681
741
|
}
|
|
682
742
|
|
|
743
|
+
/// Block until this exact sandbox reaches `status` (no built-in timeout),
|
|
744
|
+
/// returning a fresh handle. A same-name replacement raises
|
|
745
|
+
/// `SandboxReplacedError` instead of silently redirecting the wait.
|
|
746
|
+
fn wait_for_status(&self, status: String) -> Result<SbHandle, Error> {
|
|
747
|
+
let status = sandbox_status_from_str(&status)?;
|
|
748
|
+
let handle = block_on(self.inner.wait_for_status(status)).map_err(error::to_ruby)?;
|
|
749
|
+
Ok(SbHandle::from_inner(handle))
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/// Stop and start this exact sandbox, returning the new live sandbox.
|
|
753
|
+
/// `opts` carries `force`/`timeout`/`detached`.
|
|
754
|
+
fn restart(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
755
|
+
let options = restart_options(opts)?;
|
|
756
|
+
let inner = block_on(self.inner.restart_with(options)).map_err(error::to_ruby)?;
|
|
757
|
+
Ok(Sandbox::from_inner(inner))
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/// Stop and remove this exact sandbox. `opts` carries `force`/`timeout`.
|
|
761
|
+
fn destroy(&self, opts: RHash) -> Result<(), Error> {
|
|
762
|
+
let options = destroy_options(opts)?;
|
|
763
|
+
block_on(self.inner.destroy_with(options)).map_err(error::to_ruby)
|
|
764
|
+
}
|
|
765
|
+
|
|
683
766
|
/// Live status fetched from the backend (a round-trip per call).
|
|
684
767
|
fn status(&self) -> Result<String, Error> {
|
|
685
768
|
let status = block_on(self.inner.status()).map_err(error::to_ruby)?;
|
|
@@ -1362,6 +1445,63 @@ fn parse_dns(d: RHash) -> Result<DnsSpec, Error> {
|
|
|
1362
1445
|
})
|
|
1363
1446
|
}
|
|
1364
1447
|
|
|
1448
|
+
/// Apply the normalized `proxy` create option (v0.6.17) to the builder.
|
|
1449
|
+
/// Mirrors `sdk/python/src/helpers.rs`: `socks4` takes an optional `user_id`,
|
|
1450
|
+
/// `socks5` optional `credentials` whose password is an env-backed
|
|
1451
|
+
/// [`SecretSource`] (the only kind). Unknown protocols and non-env password
|
|
1452
|
+
/// sources are rejected here (the Ruby layer already does, so these are
|
|
1453
|
+
/// belt-and-braces for callers of the native API).
|
|
1454
|
+
fn apply_outbound_proxy(b: SandboxBuilder, proxy: RHash) -> Result<SandboxBuilder, Error> {
|
|
1455
|
+
let protocol = conv::opt_string(proxy, "protocol")?
|
|
1456
|
+
.ok_or_else(|| error::base_error("proxy requires protocol:"))?;
|
|
1457
|
+
let address = conv::opt_string(proxy, "address")?
|
|
1458
|
+
.ok_or_else(|| error::base_error("proxy requires address:"))?;
|
|
1459
|
+
match protocol.as_str() {
|
|
1460
|
+
"socks4" => {
|
|
1461
|
+
let user_id = conv::opt_string(proxy, "user_id")?;
|
|
1462
|
+
Ok(b.proxy(move |p| {
|
|
1463
|
+
let p = p.socks4(address);
|
|
1464
|
+
match user_id {
|
|
1465
|
+
Some(user_id) => p.user_id(user_id),
|
|
1466
|
+
None => p,
|
|
1467
|
+
}
|
|
1468
|
+
}))
|
|
1469
|
+
}
|
|
1470
|
+
"socks5" => {
|
|
1471
|
+
let credentials = match conv::opt::<RHash>(proxy, "credentials")? {
|
|
1472
|
+
None => None,
|
|
1473
|
+
Some(c) => {
|
|
1474
|
+
let username = conv::opt_string(c, "username")?
|
|
1475
|
+
.ok_or_else(|| error::base_error("SOCKS5 credentials require username:"))?;
|
|
1476
|
+
let password = conv::opt::<RHash>(c, "password")?
|
|
1477
|
+
.ok_or_else(|| error::base_error("SOCKS5 credentials require password:"))?;
|
|
1478
|
+
let kind = conv::opt_string(password, "kind")?.ok_or_else(|| {
|
|
1479
|
+
error::base_error("SOCKS5 password source requires kind:")
|
|
1480
|
+
})?;
|
|
1481
|
+
if kind != "env" {
|
|
1482
|
+
return Err(error::base_error(format!(
|
|
1483
|
+
"unsupported SOCKS5 password source {kind:?}; only env is supported"
|
|
1484
|
+
)));
|
|
1485
|
+
}
|
|
1486
|
+
let var = conv::opt_string(password, "var")?
|
|
1487
|
+
.ok_or_else(|| error::base_error("SOCKS5 password source requires var:"))?;
|
|
1488
|
+
Some((username, SecretSource::env(var)))
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
Ok(b.proxy(move |p| {
|
|
1492
|
+
let p = p.socks5(address);
|
|
1493
|
+
match credentials {
|
|
1494
|
+
Some((username, password)) => p.credentials(username, password),
|
|
1495
|
+
None => p,
|
|
1496
|
+
}
|
|
1497
|
+
}))
|
|
1498
|
+
}
|
|
1499
|
+
other => Err(error::base_error(format!(
|
|
1500
|
+
"unsupported outbound proxy protocol {other:?}; expected socks4 or socks5"
|
|
1501
|
+
))),
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1365
1505
|
/// One token bucket of the `rate_limiter` create option (v0.6.9):
|
|
1366
1506
|
/// `(size, refill_time_ms, one_time_burst)`. Size is bytes for bandwidth
|
|
1367
1507
|
/// buckets, frames for ops buckets.
|
|
@@ -2096,6 +2236,55 @@ fn sandbox_status_str(status: SandboxStatus) -> &'static str {
|
|
|
2096
2236
|
}
|
|
2097
2237
|
}
|
|
2098
2238
|
|
|
2239
|
+
/// Parse a Ruby-facing status name back into a core `SandboxStatus`. The Ruby
|
|
2240
|
+
/// layer already validates the seven names (raising `ArgumentError`, the repo's
|
|
2241
|
+
/// idiom for a bad argument value), so reaching the fallback here means the
|
|
2242
|
+
/// native layer was called directly; keep it exhaustive rather than silent.
|
|
2243
|
+
fn sandbox_status_from_str(status: &str) -> Result<SandboxStatus, Error> {
|
|
2244
|
+
match status {
|
|
2245
|
+
"created" => Ok(SandboxStatus::Created),
|
|
2246
|
+
"starting" => Ok(SandboxStatus::Starting),
|
|
2247
|
+
"running" => Ok(SandboxStatus::Running),
|
|
2248
|
+
"draining" => Ok(SandboxStatus::Draining),
|
|
2249
|
+
"paused" => Ok(SandboxStatus::Paused),
|
|
2250
|
+
"stopped" => Ok(SandboxStatus::Stopped),
|
|
2251
|
+
"crashed" => Ok(SandboxStatus::Crashed),
|
|
2252
|
+
other => Err(error::base_error(format!(
|
|
2253
|
+
"unknown sandbox status {other:?} (expected created/starting/running/draining/\
|
|
2254
|
+
paused/stopped/crashed)"
|
|
2255
|
+
))),
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
/// Build the core's `RestartOptions` from a string-keyed Hash
|
|
2260
|
+
/// (`force`/`timeout`/`detached`). An absent `timeout` keeps the core's
|
|
2261
|
+
/// ten-second graceful-shutdown default.
|
|
2262
|
+
fn restart_options(opts: RHash) -> Result<RestartOptions, Error> {
|
|
2263
|
+
let mut options = RestartOptions {
|
|
2264
|
+
force: conv::opt_bool(opts, "force")?,
|
|
2265
|
+
detached: conv::opt_bool(opts, "detached")?,
|
|
2266
|
+
..Default::default()
|
|
2267
|
+
};
|
|
2268
|
+
if let Some(secs) = conv::opt_f64(opts, "timeout")? {
|
|
2269
|
+
options.timeout = secs_to_duration(secs)?;
|
|
2270
|
+
}
|
|
2271
|
+
Ok(options)
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/// Build the core's `DestroyOptions` from a string-keyed Hash
|
|
2275
|
+
/// (`force`/`timeout`). An absent `timeout` keeps the core's ten-second
|
|
2276
|
+
/// graceful-shutdown default.
|
|
2277
|
+
fn destroy_options(opts: RHash) -> Result<DestroyOptions, Error> {
|
|
2278
|
+
let mut options = DestroyOptions {
|
|
2279
|
+
force: conv::opt_bool(opts, "force")?,
|
|
2280
|
+
..Default::default()
|
|
2281
|
+
};
|
|
2282
|
+
if let Some(secs) = conv::opt_f64(opts, "timeout")? {
|
|
2283
|
+
options.timeout = secs_to_duration(secs)?;
|
|
2284
|
+
}
|
|
2285
|
+
Ok(options)
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2099
2288
|
/// A `std::process::ExitStatus` as a Ruby Hash: `exit_code` (Integer or nil) and
|
|
2100
2289
|
/// `success` (Boolean). Returned by the live `Sandbox#wait` / `#stop_and_wait`.
|
|
2101
2290
|
fn exit_status_to_hash(status: std::process::ExitStatus) -> RHash {
|
|
@@ -2422,6 +2611,13 @@ impl SbHandle {
|
|
|
2422
2611
|
self.inner.name().to_string()
|
|
2423
2612
|
}
|
|
2424
2613
|
|
|
2614
|
+
/// The opaque, backend-assigned identity of the persisted sandbox this
|
|
2615
|
+
/// handle is bound to (v0.6.16). The lifecycle operations below refuse to
|
|
2616
|
+
/// act on a same-name replacement by comparing against it.
|
|
2617
|
+
fn id(&self) -> String {
|
|
2618
|
+
self.inner.id().to_string()
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2425
2621
|
/// Status snapshot captured when the handle was fetched (synchronous).
|
|
2426
2622
|
fn status(&self) -> String {
|
|
2427
2623
|
sandbox_status_str(self.inner.status_snapshot()).to_string()
|
|
@@ -2470,6 +2666,45 @@ impl SbHandle {
|
|
|
2470
2666
|
block_on(self.inner.request_drain()).map_err(error::to_ruby)
|
|
2471
2667
|
}
|
|
2472
2668
|
|
|
2669
|
+
/// Connect when this exact sandbox is running, wait while it is starting,
|
|
2670
|
+
/// or start it when it is created/stopped/crashed. `opts` carries
|
|
2671
|
+
/// `detached`, which applies only when a start is actually required.
|
|
2672
|
+
fn connect_or_start(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
2673
|
+
let detached = conv::opt_bool(opts, "detached")?;
|
|
2674
|
+
let inner = block_on(async {
|
|
2675
|
+
if detached {
|
|
2676
|
+
self.inner.connect_or_start_detached().await
|
|
2677
|
+
} else {
|
|
2678
|
+
self.inner.connect_or_start().await
|
|
2679
|
+
}
|
|
2680
|
+
})
|
|
2681
|
+
.map_err(error::to_ruby)?;
|
|
2682
|
+
Ok(Sandbox::from_inner(inner))
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/// Block until this exact sandbox reaches `status` (no built-in timeout),
|
|
2686
|
+
/// returning a fresh handle. A same-name replacement raises
|
|
2687
|
+
/// `SandboxReplacedError` instead of silently redirecting the wait.
|
|
2688
|
+
fn wait_for_status(&self, status: String) -> Result<SbHandle, Error> {
|
|
2689
|
+
let status = sandbox_status_from_str(&status)?;
|
|
2690
|
+
let handle = block_on(self.inner.wait_for_status(status)).map_err(error::to_ruby)?;
|
|
2691
|
+
Ok(SbHandle::from_inner(handle))
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
/// Stop and start this exact sandbox, returning the new live sandbox.
|
|
2695
|
+
/// `opts` carries `force`/`timeout`/`detached`.
|
|
2696
|
+
fn restart(&self, opts: RHash) -> Result<Sandbox, Error> {
|
|
2697
|
+
let options = restart_options(opts)?;
|
|
2698
|
+
let inner = block_on(self.inner.restart_with(options)).map_err(error::to_ruby)?;
|
|
2699
|
+
Ok(Sandbox::from_inner(inner))
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
/// Stop and remove this exact sandbox. `opts` carries `force`/`timeout`.
|
|
2703
|
+
fn destroy(&self, opts: RHash) -> Result<(), Error> {
|
|
2704
|
+
let options = destroy_options(opts)?;
|
|
2705
|
+
block_on(self.inner.destroy_with(options)).map_err(error::to_ruby)
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2473
2708
|
/// Block until the sandbox reaches a terminal state; returns a stop-result
|
|
2474
2709
|
/// Hash (name, status, exit_code, signal, observed_at_ms, source).
|
|
2475
2710
|
fn wait_until_stopped(&self) -> Result<RHash, Error> {
|
|
@@ -2607,6 +2842,10 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2607
2842
|
"create_with_progress",
|
|
2608
2843
|
function!(Sandbox::create_with_progress, 2),
|
|
2609
2844
|
)?;
|
|
2845
|
+
class.define_singleton_method(
|
|
2846
|
+
"connect_or_create",
|
|
2847
|
+
function!(Sandbox::connect_or_create, 2),
|
|
2848
|
+
)?;
|
|
2610
2849
|
class.define_singleton_method("start", function!(Sandbox::start, 2))?;
|
|
2611
2850
|
class.define_singleton_method("get", function!(Sandbox::get, 1))?;
|
|
2612
2851
|
class.define_singleton_method("list", function!(Sandbox::list, 0))?;
|
|
@@ -2614,6 +2853,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2614
2853
|
class.define_singleton_method("remove", function!(Sandbox::remove, 1))?;
|
|
2615
2854
|
|
|
2616
2855
|
class.define_method("name", method!(Sandbox::name, 0))?;
|
|
2856
|
+
class.define_method("id", method!(Sandbox::id, 0))?;
|
|
2617
2857
|
class.define_method("exec", method!(Sandbox::exec, 3))?;
|
|
2618
2858
|
class.define_method("shell", method!(Sandbox::shell, 2))?;
|
|
2619
2859
|
class.define_method("exec_stream", method!(Sandbox::exec_stream, 3))?;
|
|
@@ -2628,6 +2868,9 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2628
2868
|
class.define_method("kill", method!(Sandbox::kill, 0))?;
|
|
2629
2869
|
class.define_method("drain", method!(Sandbox::drain, 0))?;
|
|
2630
2870
|
class.define_method("wait", method!(Sandbox::wait, 0))?;
|
|
2871
|
+
class.define_method("wait_for_status", method!(Sandbox::wait_for_status, 1))?;
|
|
2872
|
+
class.define_method("restart", method!(Sandbox::restart, 1))?;
|
|
2873
|
+
class.define_method("destroy", method!(Sandbox::destroy, 1))?;
|
|
2631
2874
|
class.define_method("status", method!(Sandbox::status, 0))?;
|
|
2632
2875
|
class.define_method("owns_lifecycle", method!(Sandbox::owns_lifecycle, 0))?;
|
|
2633
2876
|
class.define_method("detach", method!(Sandbox::detach, 0))?;
|
|
@@ -2667,6 +2910,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2667
2910
|
|
|
2668
2911
|
let handle = native.define_class("SandboxHandle", ruby.class_object())?;
|
|
2669
2912
|
handle.define_method("name", method!(SbHandle::name, 0))?;
|
|
2913
|
+
handle.define_method("id", method!(SbHandle::id, 0))?;
|
|
2670
2914
|
handle.define_method("status", method!(SbHandle::status, 0))?;
|
|
2671
2915
|
handle.define_method("created_at_ms", method!(SbHandle::created_at_ms, 0))?;
|
|
2672
2916
|
handle.define_method("updated_at_ms", method!(SbHandle::updated_at_ms, 0))?;
|
|
@@ -2674,6 +2918,10 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
2674
2918
|
handle.define_method("stop_with_timeout", method!(SbHandle::stop_with_timeout, 1))?;
|
|
2675
2919
|
handle.define_method("kill", method!(SbHandle::kill, 0))?;
|
|
2676
2920
|
handle.define_method("kill_with_timeout", method!(SbHandle::kill_with_timeout, 1))?;
|
|
2921
|
+
handle.define_method("connect_or_start", method!(SbHandle::connect_or_start, 1))?;
|
|
2922
|
+
handle.define_method("wait_for_status", method!(SbHandle::wait_for_status, 1))?;
|
|
2923
|
+
handle.define_method("restart", method!(SbHandle::restart, 1))?;
|
|
2924
|
+
handle.define_method("destroy", method!(SbHandle::destroy, 1))?;
|
|
2677
2925
|
handle.define_method("request_stop", method!(SbHandle::request_stop, 0))?;
|
|
2678
2926
|
handle.define_method("request_kill", method!(SbHandle::request_kill, 0))?;
|
|
2679
2927
|
handle.define_method("request_drain", method!(SbHandle::request_drain, 0))?;
|
data/lib/microsandbox/errors.rb
CHANGED
|
@@ -35,6 +35,13 @@ module Microsandbox
|
|
|
35
35
|
define_error(:SandboxNotRunningError, "sandbox-not-running")
|
|
36
36
|
define_error(:SandboxAlreadyExistsError, "sandbox-already-exists")
|
|
37
37
|
define_error(:SandboxStillRunningError, "sandbox-still-running")
|
|
38
|
+
# v0.6.16: a lifecycle operation was aimed at a sandbox identity that no
|
|
39
|
+
# longer owns the name — the name was removed and recreated behind the
|
|
40
|
+
# caller's back. Raised by the identity-checked convergent APIs
|
|
41
|
+
# (`wait_for_status`, `restart`, `destroy`, `connect_or_start`) rather than
|
|
42
|
+
# letting them act on the replacement. Mirrors the Python SDK's
|
|
43
|
+
# SandboxReplacedError.
|
|
44
|
+
define_error(:SandboxReplacedError, "sandbox-replaced")
|
|
38
45
|
|
|
39
46
|
# Execution errors --------------------------------------------------------
|
|
40
47
|
define_error(:ExecTimeoutError, "exec-timeout")
|