microsandbox-rb 0.6.0 → 0.8.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 +138 -1
- data/Cargo.lock +25 -25
- data/DESIGN.md +16 -6
- data/README.md +31 -16
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/src/agent.rs +18 -7
- data/ext/microsandbox/src/conv.rs +37 -1
- data/ext/microsandbox/src/fs_stream.rs +92 -0
- data/ext/microsandbox/src/image.rs +6 -0
- data/ext/microsandbox/src/lib.rs +40 -0
- data/ext/microsandbox/src/sandbox.rs +673 -64
- data/ext/microsandbox/src/snapshot.rs +72 -6
- data/ext/microsandbox/src/volume.rs +113 -1
- data/lib/microsandbox/agent.rb +3 -1
- data/lib/microsandbox/exec_handle.rb +7 -3
- data/lib/microsandbox/exec_output.rb +7 -7
- data/lib/microsandbox/fs.rb +84 -2
- data/lib/microsandbox/image.rb +2 -1
- data/lib/microsandbox/log_entry.rb +4 -2
- data/lib/microsandbox/metrics.rb +9 -0
- data/lib/microsandbox/sandbox.rb +461 -70
- data/lib/microsandbox/snapshot.rb +63 -6
- data/lib/microsandbox/ssh.rb +14 -9
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +100 -1
- data/lib/microsandbox.rb +35 -1
- data/sig/microsandbox.rbs +70 -6
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 06dc745a315d5d79f21e702032d45a984dbf53110e3375bb560de9cb008866d6
|
|
4
|
+
data.tar.gz: 9168ece64600356ce88ed3c3ed42cc88f4354f3619abfdc32fabd578745cf6c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8fed4d0957be541dd5de600dd24386ff7b62d9933e45e244a49200185989cb8427279c80234038a4355aee296fdc96ec1cd5738e4bcf3fae48b27388f7dcad5
|
|
7
|
+
data.tar.gz: 9310a3b2d3a0322e701bc85cbfe6b820576b4254c3d49099bbfcab07690421c1e2d9705d0106e1d2931fb3516a0d462401758d5393552810427a6891ed6fc79e
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,138 @@ wraps, and the README's Versioning section keeps the full gem→runtime map.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.8.0] - 2026-06-25
|
|
12
|
+
|
|
13
|
+
Adopts upstream runtime **`v0.5.10`** (up from the `v0.5.8` that `0.7.0` shipped).
|
|
14
|
+
Runtime-only bump — no public Ruby API change.
|
|
15
|
+
|
|
16
|
+
### Runtime
|
|
17
|
+
|
|
18
|
+
- **Adopted upstream `v0.5.10`** — the `microsandbox`/`microsandbox-network` git
|
|
19
|
+
deps and `Microsandbox::RUNTIME_VERSION` now pin `v0.5.10`. This is the runtime
|
|
20
|
+
bump originally attempted against `v0.5.9` during the `0.7.0` cycle and reverted:
|
|
21
|
+
upstream's `v0.5.9` git tag predated its own crate-version bump, so the prebuilt
|
|
22
|
+
runtime-provisioning path (`PREBUILT_VERSION = env!("CARGO_PKG_VERSION")`)
|
|
23
|
+
resolved to `0.5.8` and downloaded a `msb` that rejected the new `--config-fd`
|
|
24
|
+
flag the SDK unconditionally passes — every `Sandbox.create` died at boot.
|
|
25
|
+
Upstream chose not to re-tag (most package registries forbid republishing a tag)
|
|
26
|
+
and instead cut a clean **`v0.5.10`** whose tag carries the matching crate
|
|
27
|
+
version `0.5.10` (upstream
|
|
28
|
+
[#1029](https://github.com/superradcompany/microsandbox/issues/1029)). The bump
|
|
29
|
+
carries the following upstream changes:
|
|
30
|
+
- **Heartbeat no longer reclaims busy sandboxes** (upstream #1011). The host
|
|
31
|
+
watchdog is now idle-detection only — a healthy sandbox with an active (or
|
|
32
|
+
briefly starved) `exec` session is never killed for a stale heartbeat, the
|
|
33
|
+
way it could be before.
|
|
34
|
+
- **Launch config moved off the process argv** (upstream #1006). Bulky and
|
|
35
|
+
secret-bearing config (the network blob, env) is handed to the sandbox over
|
|
36
|
+
an inherited, unlinked-tempfile fd instead of `--`-flags, so it no longer
|
|
37
|
+
leaks into `ps` / `/proc/<pid>/cmdline`.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **Directory bind mounts now carry a default 4 GiB guest-write quota**
|
|
42
|
+
(upstream #1020). Any `volumes:` entry that binds a host directory (e.g.
|
|
43
|
+
`volumes: { "/out" => "/host/out" }`) is given a `DEFAULT_BIND_QUOTA_MIB`
|
|
44
|
+
(4096 MiB) guest-write budget by the v0.5.10 runtime when no explicit quota is
|
|
45
|
+
set, so a sandbox can no longer fill the host disk through a bind mount. This
|
|
46
|
+
is a **behavior change**: a workload that wrote more than 4 GiB to a bind mount
|
|
47
|
+
under the `v0.5.8` runtime (`0.7.0`) will now fail with `ENOSPC`. The gem does
|
|
48
|
+
not yet expose a per-bind quota override (named-volume `Volume.create` accepts
|
|
49
|
+
`quota_mib:`, but the inline bind-mount path does not) — that escape hatch is a
|
|
50
|
+
tracked follow-up. Until then, route large-write mounts through a named volume
|
|
51
|
+
with an explicit `quota_mib:`.
|
|
52
|
+
|
|
53
|
+
## [0.7.0] - 2026-06-23
|
|
54
|
+
|
|
55
|
+
A large parity release closing the binding gaps an audit against the upstream
|
|
56
|
+
Python/Node SDKs (at the wrapped `v0.5.8` runtime) surfaced. The runtime tag is
|
|
57
|
+
unchanged. Two genuine bug fixes; the rest is newly-exposed surface plus a few
|
|
58
|
+
behavior corrections (see **Changed**).
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **Lossy UTF-8 decoding.** `LogEntry#text`, `ExecOutput#stdout`/`#stderr`,
|
|
63
|
+
`ExecEvent#text`, `SshOutput#stdout`/`#stderr`, and `SftpClient#read_text` now
|
|
64
|
+
scrub invalid byte sequences (replacing them with U+FFFD) so they always
|
|
65
|
+
return a *valid* UTF-8 String — matching the Python/Node SDKs. Previously they
|
|
66
|
+
re-tagged raw bytes as UTF-8 without transcoding, so captured output
|
|
67
|
+
containing invalid UTF-8 produced strings that raised downstream (regex,
|
|
68
|
+
concatenation, `JSON.generate`). Raw bytes remain available via `#data` /
|
|
69
|
+
`#stdout_bytes` / `#stderr_bytes`.
|
|
70
|
+
- **`runtime_path=` spec** no longer pollutes the process-wide set-once
|
|
71
|
+
`msb`-path `OnceLock` (it now stubs the native setter), removing an
|
|
72
|
+
order-dependent failure in combined unit+integration runs.
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- **Streaming image-pull progress** — `Sandbox.create_with_progress` returns a
|
|
77
|
+
`PullSession` (an `Enumerable` of progress-event Hashes) with `#sandbox` for
|
|
78
|
+
the booted sandbox.
|
|
79
|
+
- **Host-side volume filesystem** — `Volume.fs(name)` / `VolumeInfo#fs` return a
|
|
80
|
+
`VolumeFs` (read/read_text/write/list/mkdir/remove_file/remove_dir/exists?/
|
|
81
|
+
copy/rename/stat) that reads and writes a named volume without a running
|
|
82
|
+
sandbox.
|
|
83
|
+
- **Streaming guest filesystem** — `FS#read_stream` / `FS#write_stream`
|
|
84
|
+
(`FsReadStream`/`FsWriteSink`) for files too large to buffer in memory.
|
|
85
|
+
- **Full secrets surface** — `secrets:` entries accept `hosts:` / `host_patterns:`
|
|
86
|
+
(wildcards) allow-lists, `placeholder:`, `require_tls:`, injection toggles
|
|
87
|
+
(`inject_headers:`/`inject_basic_auth:`/`inject_query:`/`inject_body:`), and
|
|
88
|
+
per-secret `on_violation:`; plus a sandbox-level `on_secret_violation:`. The
|
|
89
|
+
block-variant actions accept both the underscore form (`block_and_log`) and the
|
|
90
|
+
upstream kebab-case wire spelling (`block-and-log`) used by the CLI / Go SDK /
|
|
91
|
+
config files; the bare `"passthrough"` string (passthrough-all-hosts, as in the
|
|
92
|
+
Python/Node SDKs) is also accepted, so a policy copied from another SDK ports
|
|
93
|
+
over unchanged.
|
|
94
|
+
- **Network configuration** — `Sandbox.create` now accepts `dns:` (nameservers/
|
|
95
|
+
rebind_protection/query_timeout_ms), `tls:` (interception tuning incl. bypass
|
|
96
|
+
patterns, intercepted ports, block_quic, and CA cert/key paths), `ipv4_pool:`/
|
|
97
|
+
`ipv6_pool:`, `max_connections:`, and `trust_host_cas:`.
|
|
98
|
+
- **Create options** — `init:`/`init_with` (hand guest PID 1 to an init system),
|
|
99
|
+
`ephemeral:` (auto-remove state on terminal), and disk-image `fstype:`.
|
|
100
|
+
`fstype:` is rejected up front unless `image:` is a disk-image path (a local
|
|
101
|
+
path ending in `.raw`/`.qcow2`/`.vmdk`); pairing it with an OCI reference no
|
|
102
|
+
longer routes the ref through the disk-image builder and fails at boot.
|
|
103
|
+
- **Full mount options** — `volumes:` now supports `{ tmpfs: }`, `{ disk:,
|
|
104
|
+
format:, fstype: }`, and per-mount `stat_virtualization:`/`host_permissions:`
|
|
105
|
+
alongside the existing bind/named + ro/noexec/nosuid/nodev flags. The pre-0.7.0
|
|
106
|
+
`options: %w[ro noexec]` array form is still honored (translated onto the
|
|
107
|
+
boolean flags); an unrecognized token now raises rather than being silently
|
|
108
|
+
dropped, so a requested read-only/noexec mount can't quietly become writable.
|
|
109
|
+
- **Snapshots** — `Snapshot.open`/`list_dir`/`reindex`, `SnapshotInfo#open`/
|
|
110
|
+
`#remove`, and `SandboxHandle#snapshot`/`#snapshot_to`. `SnapshotInfo` now
|
|
111
|
+
carries the full manifest (`image_manifest_digest`, `fstype`,
|
|
112
|
+
`source_sandbox`, `labels`) on the artifact-opening paths.
|
|
113
|
+
- **`SandboxHandle#config` / `#config_json`** — read the stored sandbox config.
|
|
114
|
+
- **Metrics** — `upper_used_bytes`, `upper_free_bytes`,
|
|
115
|
+
`upper_host_allocated_bytes` (OCI writable-upper-layer accounting).
|
|
116
|
+
- **`ImageDetail#config["labels"]`** — OCI config labels.
|
|
117
|
+
- **`Microsandbox.setup`** — customizable runtime install (`base_dir:`,
|
|
118
|
+
`version:`, `force:`, `skip_verify:`); `force:` repairs a corrupt install.
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
|
|
122
|
+
- **`exec`/`shell` stdin** is now a closed set: `nil`/`:null` = no stdin,
|
|
123
|
+
`:pipe` = streaming pipe (streaming variants only), a String = bytes. An
|
|
124
|
+
unrecognized Symbol now raises `ArgumentError` instead of being fed as its
|
|
125
|
+
characters (so a mistaken `stdin: :null` no longer sends the literal `"null"`).
|
|
126
|
+
- **Write methods reject non-Strings.** `FS#write`, `SftpClient#write`,
|
|
127
|
+
`ExecStdin#write`, `VolumeFs#write`, and `FsWriteSink#write` now raise
|
|
128
|
+
`TypeError` for non-String data instead of silently writing its `to_s` form.
|
|
129
|
+
- **Agent connect timeout.** `AgentClient.connect_sandbox`/`connect_path`
|
|
130
|
+
`timeout:` now treats `0` as an immediate deadline and raises on a
|
|
131
|
+
negative/non-finite value, instead of silently falling back to the default.
|
|
132
|
+
- **Secrets shorthand** still accepts `{ env:, value:, host: }`; the validation
|
|
133
|
+
message changed and a host allow-list is now required.
|
|
134
|
+
|
|
135
|
+
### Docs
|
|
136
|
+
|
|
137
|
+
- README/DESIGN implemented-surface corrected to match the binding (and to list
|
|
138
|
+
the few secondary knobs still not exposed); assorted YARD fixes
|
|
139
|
+
(`runtime_path=` set-once note, `VolumeInfo#kind` `:dir`, `create`'s
|
|
140
|
+
`volumes:`/`from_snapshot:` params, `log_stream` `'all'` source); CHANGELOG
|
|
141
|
+
compare links added for 0.5.9–0.5.12.
|
|
142
|
+
|
|
11
143
|
## [0.6.0] - 2026-06-23
|
|
12
144
|
|
|
13
145
|
This release puts the gem on its **own semantic version**, decoupled from the
|
|
@@ -291,7 +423,12 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
|
|
|
291
423
|
core crate has Apple-native deps). Until precompiled gems are published,
|
|
292
424
|
installing from source requires a Rust toolchain (stable >= 1.91).
|
|
293
425
|
|
|
294
|
-
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.
|
|
426
|
+
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.7.0...HEAD
|
|
427
|
+
[0.7.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.6.0...v0.7.0
|
|
295
428
|
[0.6.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.12...v0.6.0
|
|
429
|
+
[0.5.12]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.11...v0.5.12
|
|
430
|
+
[0.5.11]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.10...v0.5.11
|
|
431
|
+
[0.5.10]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.9...v0.5.10
|
|
432
|
+
[0.5.9]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.8...v0.5.9
|
|
296
433
|
[0.5.8]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.7...v0.5.8
|
|
297
434
|
[0.5.7]: https://github.com/superradcompany/microsandbox/releases/tag/v0.5.7
|
data/Cargo.lock
CHANGED
|
@@ -3003,8 +3003,8 @@ dependencies = [
|
|
|
3003
3003
|
|
|
3004
3004
|
[[package]]
|
|
3005
3005
|
name = "microsandbox"
|
|
3006
|
-
version = "0.5.
|
|
3007
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3006
|
+
version = "0.5.10"
|
|
3007
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3008
3008
|
dependencies = [
|
|
3009
3009
|
"anyhow",
|
|
3010
3010
|
"astral-tokio-tar",
|
|
@@ -3056,8 +3056,8 @@ dependencies = [
|
|
|
3056
3056
|
|
|
3057
3057
|
[[package]]
|
|
3058
3058
|
name = "microsandbox-agent-client"
|
|
3059
|
-
version = "0.5.
|
|
3060
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3059
|
+
version = "0.5.10"
|
|
3060
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3061
3061
|
dependencies = [
|
|
3062
3062
|
"ciborium",
|
|
3063
3063
|
"microsandbox-protocol",
|
|
@@ -3069,8 +3069,8 @@ dependencies = [
|
|
|
3069
3069
|
|
|
3070
3070
|
[[package]]
|
|
3071
3071
|
name = "microsandbox-db"
|
|
3072
|
-
version = "0.5.
|
|
3073
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3072
|
+
version = "0.5.10"
|
|
3073
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3074
3074
|
dependencies = [
|
|
3075
3075
|
"async-trait",
|
|
3076
3076
|
"sea-orm",
|
|
@@ -3081,8 +3081,8 @@ dependencies = [
|
|
|
3081
3081
|
|
|
3082
3082
|
[[package]]
|
|
3083
3083
|
name = "microsandbox-filesystem"
|
|
3084
|
-
version = "0.5.
|
|
3085
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3084
|
+
version = "0.5.10"
|
|
3085
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3086
3086
|
dependencies = [
|
|
3087
3087
|
"libc",
|
|
3088
3088
|
"microsandbox-utils",
|
|
@@ -3094,8 +3094,8 @@ dependencies = [
|
|
|
3094
3094
|
|
|
3095
3095
|
[[package]]
|
|
3096
3096
|
name = "microsandbox-image"
|
|
3097
|
-
version = "0.5.
|
|
3098
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3097
|
+
version = "0.5.10"
|
|
3098
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3099
3099
|
dependencies = [
|
|
3100
3100
|
"astral-tokio-tar",
|
|
3101
3101
|
"async-compression",
|
|
@@ -3120,8 +3120,8 @@ dependencies = [
|
|
|
3120
3120
|
|
|
3121
3121
|
[[package]]
|
|
3122
3122
|
name = "microsandbox-metrics"
|
|
3123
|
-
version = "0.5.
|
|
3124
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3123
|
+
version = "0.5.10"
|
|
3124
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3125
3125
|
dependencies = [
|
|
3126
3126
|
"chrono",
|
|
3127
3127
|
"libc",
|
|
@@ -3131,8 +3131,8 @@ dependencies = [
|
|
|
3131
3131
|
|
|
3132
3132
|
[[package]]
|
|
3133
3133
|
name = "microsandbox-migration"
|
|
3134
|
-
version = "0.5.
|
|
3135
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3134
|
+
version = "0.5.10"
|
|
3135
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3136
3136
|
dependencies = [
|
|
3137
3137
|
"sea-orm-migration",
|
|
3138
3138
|
"serde_json",
|
|
@@ -3140,8 +3140,8 @@ dependencies = [
|
|
|
3140
3140
|
|
|
3141
3141
|
[[package]]
|
|
3142
3142
|
name = "microsandbox-network"
|
|
3143
|
-
version = "0.5.
|
|
3144
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3143
|
+
version = "0.5.10"
|
|
3144
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3145
3145
|
dependencies = [
|
|
3146
3146
|
"base64",
|
|
3147
3147
|
"bytes",
|
|
@@ -3180,8 +3180,8 @@ dependencies = [
|
|
|
3180
3180
|
|
|
3181
3181
|
[[package]]
|
|
3182
3182
|
name = "microsandbox-protocol"
|
|
3183
|
-
version = "0.5.
|
|
3184
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3183
|
+
version = "0.5.10"
|
|
3184
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3185
3185
|
dependencies = [
|
|
3186
3186
|
"chrono",
|
|
3187
3187
|
"ciborium",
|
|
@@ -3195,8 +3195,8 @@ dependencies = [
|
|
|
3195
3195
|
|
|
3196
3196
|
[[package]]
|
|
3197
3197
|
name = "microsandbox-runtime"
|
|
3198
|
-
version = "0.5.
|
|
3199
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3198
|
+
version = "0.5.10"
|
|
3199
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3200
3200
|
dependencies = [
|
|
3201
3201
|
"bytes",
|
|
3202
3202
|
"chrono",
|
|
@@ -3225,8 +3225,8 @@ dependencies = [
|
|
|
3225
3225
|
|
|
3226
3226
|
[[package]]
|
|
3227
3227
|
name = "microsandbox-types"
|
|
3228
|
-
version = "0.5.
|
|
3229
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3228
|
+
version = "0.5.10"
|
|
3229
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3230
3230
|
dependencies = [
|
|
3231
3231
|
"chrono",
|
|
3232
3232
|
"serde",
|
|
@@ -3237,8 +3237,8 @@ dependencies = [
|
|
|
3237
3237
|
|
|
3238
3238
|
[[package]]
|
|
3239
3239
|
name = "microsandbox-utils"
|
|
3240
|
-
version = "0.5.
|
|
3241
|
-
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.
|
|
3240
|
+
version = "0.5.10"
|
|
3241
|
+
source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
|
|
3242
3242
|
dependencies = [
|
|
3243
3243
|
"dirs",
|
|
3244
3244
|
"libc",
|
|
@@ -3249,7 +3249,7 @@ dependencies = [
|
|
|
3249
3249
|
|
|
3250
3250
|
[[package]]
|
|
3251
3251
|
name = "microsandbox_rb"
|
|
3252
|
-
version = "0.
|
|
3252
|
+
version = "0.8.0"
|
|
3253
3253
|
dependencies = [
|
|
3254
3254
|
"chrono",
|
|
3255
3255
|
"futures",
|
data/DESIGN.md
CHANGED
|
@@ -183,9 +183,19 @@ The binding is verified at four levels:
|
|
|
183
183
|
microVM, confirming the gem manifest and source-install path are complete.
|
|
184
184
|
|
|
185
185
|
**Roadmap:** the v1 roadmap (custom per-rule network policies, file patches,
|
|
186
|
-
interactive `attach`/`attach_shell`, SSH, and the raw agent client) is
|
|
187
|
-
implemented
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
interactive `attach`/`attach_shell`, SSH, and the raw agent client) is
|
|
187
|
+
implemented, and so is the bulk of the v0.5.8 configuration surface that a
|
|
188
|
+
later parity pass added: streaming image-pull progress, host-side `VolumeFs`,
|
|
189
|
+
streaming guest fs (`read_stream`/`write_stream`), the full secrets surface,
|
|
190
|
+
network configuration (DNS, TLS-interception tuning, IPv4/IPv6 pools,
|
|
191
|
+
`max_connections`, `trust_host_cas`), `init`/`ephemeral`/disk-image `fstype`
|
|
192
|
+
create options, full mount options (tmpfs/disk + stat-virtualization/
|
|
193
|
+
host-permissions), and snapshot inspection (`open`/`list_dir`/`reindex`).
|
|
194
|
+
|
|
195
|
+
A few **secondary** upstream knobs remain unexposed (a genuine binding gap, not
|
|
196
|
+
upstream-gated — they exist at the pinned `v0.5.8`): per-published-port host
|
|
197
|
+
**bind address** (ports always bind loopback), network **interface overrides**,
|
|
198
|
+
and inline **named-volume create-mode** (pre-create with `Volume.create`, then
|
|
199
|
+
mount with `{ named: }`). These slot in module-by-module exactly as the existing
|
|
200
|
+
bindings do. Beyond those, surfacing genuinely newer core features is gated on
|
|
201
|
+
advancing the pinned core-crate tag.
|
data/README.md
CHANGED
|
@@ -347,7 +347,7 @@ variable → an SDK-set override → the config file → `~/.microsandbox/bin/ms
|
|
|
347
347
|
Microsandbox.installed? # => true/false
|
|
348
348
|
Microsandbox.install # download + install the runtime (idempotent)
|
|
349
349
|
Microsandbox.runtime_path # => "/Users/you/.microsandbox/bin/msb"
|
|
350
|
-
Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override
|
|
350
|
+
Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override (set-once)
|
|
351
351
|
Microsandbox.libkrunfw_path = "/opt/microsandbox/lib/libkrunfw.dylib" # override (set-once)
|
|
352
352
|
```
|
|
353
353
|
|
|
@@ -383,8 +383,8 @@ change diverged the two numbers — the gem version is **not** a reliable indica
|
|
|
383
383
|
of the embedded runtime version. To learn which runtime a build wraps, ask it:
|
|
384
384
|
|
|
385
385
|
```ruby
|
|
386
|
-
Microsandbox::VERSION # => "0.
|
|
387
|
-
Microsandbox.runtime_version # => "v0.5.
|
|
386
|
+
Microsandbox::VERSION # => "0.8.0" (the gem's own version)
|
|
387
|
+
Microsandbox.runtime_version # => "v0.5.10" (the embedded upstream runtime tag)
|
|
388
388
|
```
|
|
389
389
|
|
|
390
390
|
| Gem version | Upstream runtime | Notes |
|
|
@@ -396,6 +396,8 @@ Microsandbox.runtime_version # => "v0.5.8" (the embedded upstream runtime tag
|
|
|
396
396
|
| `0.5.11` | `v0.5.8` | gem-only revision |
|
|
397
397
|
| `0.5.12` | `v0.5.8` | gem-only revision |
|
|
398
398
|
| `0.6.0` | `v0.5.8` | gem version decoupled from the upstream tag; adds `runtime_version` |
|
|
399
|
+
| `0.7.0` | `v0.5.8` | SDK parity release (large binding-gap closure) |
|
|
400
|
+
| `0.8.0` | `v0.5.10` | adopts upstream `v0.5.10` (idle-only heartbeat, config-fd hardening, **4 GiB default bind-mount quota**); supersedes the reverted `v0.5.9` attempt |
|
|
399
401
|
|
|
400
402
|
**Going forward** — the gem version moves on its own semver track and no longer
|
|
401
403
|
mirrors the upstream tag:
|
|
@@ -470,25 +472,38 @@ or credential setup is needed.
|
|
|
470
472
|
> Until promoted, users install the source gem (which compiles via `rb_sys`).
|
|
471
473
|
|
|
472
474
|
See [DESIGN.md](DESIGN.md) for the architecture and the implemented-surface
|
|
473
|
-
section. The binding
|
|
475
|
+
section. The binding covers the official-SDK surface: sandbox
|
|
474
476
|
lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
|
|
475
477
|
`status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
|
|
476
478
|
`stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
|
|
477
|
-
`wait_until_stopped` from
|
|
479
|
+
`wait_until_stopped`/`config`/`config_json`/`snapshot`/`snapshot_to` from
|
|
480
|
+
`Sandbox.get`, and label-filtered `list_with`),
|
|
478
481
|
backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
|
|
479
482
|
`exec`/`shell` (collected and streaming), interactive `attach`/
|
|
480
|
-
`attach_shell`, the full guest filesystem
|
|
481
|
-
`Microsandbox.all_sandbox_metrics`, and
|
|
482
|
-
logs, OCI image-cache management,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
`
|
|
487
|
-
(`Microsandbox::
|
|
488
|
-
`
|
|
489
|
-
|
|
483
|
+
`attach_shell`, the full guest filesystem (incl. streaming `read_stream`/
|
|
484
|
+
`write_stream`), metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
|
|
485
|
+
streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
|
|
486
|
+
named volumes (incl. host-side `Volume.fs`/`VolumeInfo#fs` read/write),
|
|
487
|
+
snapshots (create/open/list/list_dir/reindex/verify/export/import +
|
|
488
|
+
boot-from-snapshot), streaming image-pull progress
|
|
489
|
+
(`Sandbox.create_with_progress` → `PullSession`), **rootfs patches**
|
|
490
|
+
(`Microsandbox::Patch`), **network configuration** (presets, custom per-rule
|
|
491
|
+
`Microsandbox::NetworkPolicy`/`Rule`/`Destination`, plus DNS, TLS interception,
|
|
492
|
+
IPv4/IPv6 pools, `max_connections`, `trust_host_cas`), **secrets** (multi-host /
|
|
493
|
+
wildcard allow-lists, injection toggles, per-secret + sandbox-level violation
|
|
494
|
+
policy), **SSH** (`Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`), and the
|
|
495
|
+
**raw agent client** (`Microsandbox::AgentClient`). Create options span
|
|
496
|
+
resources, `init`/`ephemeral`, disk-image `fstype`, network policy + config,
|
|
497
|
+
`log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
|
|
498
|
+
(bind/named/tmpfs/disk with mount policies) and more; `exec`/`shell` take
|
|
499
|
+
per-call `rlimits`, and `create` accepts
|
|
490
500
|
`registry_auth`/`registry_insecure`/`registry_ca_certs` for private and
|
|
491
|
-
authenticated registries.
|
|
501
|
+
authenticated registries, plus customizable provisioning via `Microsandbox.setup`.
|
|
502
|
+
|
|
503
|
+
A few secondary upstream knobs are not yet exposed: per-published-port host bind
|
|
504
|
+
address (ports always bind loopback), network interface overrides, and inline
|
|
505
|
+
named-volume create-mode (pre-create the volume with `Volume.create`, then mount
|
|
506
|
+
it with `{ named: "…" }`).
|
|
492
507
|
|
|
493
508
|
## Contributing
|
|
494
509
|
|
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.5.
|
|
10
|
-
version = "0.
|
|
9
|
+
# The core-crate dependency below stays pinned at its own tag (v0.5.10).
|
|
10
|
+
version = "0.8.0"
|
|
11
11
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
12
12
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
13
13
|
license = "Apache-2.0"
|
|
@@ -35,8 +35,8 @@ rb-sys = "0.9"
|
|
|
35
35
|
# `.cargo/config.toml.example`). "ssh" matches the feature set the Python/Node
|
|
36
36
|
# SDKs ship with; default features add "prebuilt" (provisions msb + libkrunfw at
|
|
37
37
|
# build time), "net", and "keyring".
|
|
38
|
-
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.
|
|
39
|
-
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.
|
|
38
|
+
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.10", default-features = true, features = ["ssh"] }
|
|
39
|
+
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.10" }
|
|
40
40
|
|
|
41
41
|
# Async core bridged to Ruby's synchronous API via a blocking tokio runtime.
|
|
42
42
|
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
|
|
@@ -40,7 +40,7 @@ impl AgentClient {
|
|
|
40
40
|
|
|
41
41
|
/// Connect to a running sandbox by name. `timeout` is optional seconds.
|
|
42
42
|
fn connect_sandbox(name: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
|
|
43
|
-
let bridge = match dur(timeout) {
|
|
43
|
+
let bridge = match dur(timeout)? {
|
|
44
44
|
Some(t) => block_on(AgentBridge::connect_sandbox_with_timeout(&name, t)),
|
|
45
45
|
None => block_on(AgentBridge::connect_sandbox(&name)),
|
|
46
46
|
}
|
|
@@ -50,7 +50,7 @@ impl AgentClient {
|
|
|
50
50
|
|
|
51
51
|
/// Connect to an agentd relay socket by path. `timeout` is optional seconds.
|
|
52
52
|
fn connect_path(path: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
|
|
53
|
-
let bridge = match dur(timeout) {
|
|
53
|
+
let bridge = match dur(timeout)? {
|
|
54
54
|
Some(t) => block_on(AgentBridge::connect_path_with_timeout(&path, t)),
|
|
55
55
|
None => block_on(AgentBridge::connect_path(&path)),
|
|
56
56
|
}
|
|
@@ -125,12 +125,23 @@ impl AgentClient {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/// Convert seconds into
|
|
129
|
-
///
|
|
130
|
-
|
|
128
|
+
/// Convert an optional `timeout` (seconds) into an optional `Duration`,
|
|
129
|
+
/// mirroring the Python SDK's `timeout_duration`:
|
|
130
|
+
/// - absent (`nil`) → `None` → the core's default handshake timeout
|
|
131
|
+
/// - `0` → an explicit zero deadline (fail fast), *not* "use the default"
|
|
132
|
+
/// - negative or non-finite (NaN/Inf) → a caller error (rather than being
|
|
133
|
+
/// silently swallowed into the default)
|
|
134
|
+
/// - finite but out of `Duration` range (e.g. `Float::MAX`) → a caller error
|
|
135
|
+
/// via `try_from_secs_f64`, rather than the panic `from_secs_f64` would raise
|
|
136
|
+
fn dur(timeout: Option<f64>) -> Result<Option<Duration>, Error> {
|
|
131
137
|
match timeout {
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
None => Ok(None),
|
|
139
|
+
Some(t) if t.is_finite() && t >= 0.0 => Duration::try_from_secs_f64(t)
|
|
140
|
+
.map(Some)
|
|
141
|
+
.map_err(|e| error::base_error(format!("timeout {t} seconds is out of range: {e}"))),
|
|
142
|
+
Some(t) => Err(error::base_error(format!(
|
|
143
|
+
"timeout must be a non-negative, finite number of seconds (got {t})"
|
|
144
|
+
))),
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
use std::collections::HashMap;
|
|
7
7
|
|
|
8
|
-
use magnus::{value::ReprValue, Error, RArray, RHash, TryConvert, Value};
|
|
8
|
+
use magnus::{value::ReprValue, Error, IntoValue, RArray, RHash, TryConvert, Value};
|
|
9
9
|
|
|
10
10
|
/// Fetch a non-nil value for `key`, if present.
|
|
11
11
|
fn get(hash: RHash, key: &str) -> Option<Value> {
|
|
@@ -90,3 +90,39 @@ pub fn opt_port_map(hash: RHash, key: &str) -> Result<Vec<(u16, u16)>, Error> {
|
|
|
90
90
|
None => Ok(Vec::new()),
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/// Recursively convert a `serde_json::Value` into a Ruby value. Used for
|
|
95
|
+
/// pass-through JSON whose shape is not fixed — e.g. an image config's OCI
|
|
96
|
+
/// `labels` object, which the Ruby layer hands back verbatim (mirroring the
|
|
97
|
+
/// Python SDK's `dict | None`).
|
|
98
|
+
pub fn json_to_ruby(value: &serde_json::Value) -> Value {
|
|
99
|
+
let ruby = crate::runtime::ruby();
|
|
100
|
+
match value {
|
|
101
|
+
serde_json::Value::Null => ruby.qnil().as_value(),
|
|
102
|
+
serde_json::Value::Bool(b) => b.into_value_with(&ruby),
|
|
103
|
+
serde_json::Value::Number(n) => {
|
|
104
|
+
if let Some(i) = n.as_i64() {
|
|
105
|
+
i.into_value_with(&ruby)
|
|
106
|
+
} else if let Some(u) = n.as_u64() {
|
|
107
|
+
u.into_value_with(&ruby)
|
|
108
|
+
} else {
|
|
109
|
+
n.as_f64().unwrap_or(0.0).into_value_with(&ruby)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
serde_json::Value::String(s) => s.as_str().into_value_with(&ruby),
|
|
113
|
+
serde_json::Value::Array(items) => {
|
|
114
|
+
let arr = ruby.ary_new();
|
|
115
|
+
for item in items {
|
|
116
|
+
let _ = arr.push(json_to_ruby(item));
|
|
117
|
+
}
|
|
118
|
+
arr.into_value_with(&ruby)
|
|
119
|
+
}
|
|
120
|
+
serde_json::Value::Object(map) => {
|
|
121
|
+
let hash = ruby.hash_new();
|
|
122
|
+
for (k, v) in map {
|
|
123
|
+
let _ = hash.aset(k.as_str(), json_to_ruby(v));
|
|
124
|
+
}
|
|
125
|
+
hash.into_value_with(&ruby)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//! Streaming guest-filesystem I/O: `Microsandbox::Native::FsReadStream` and
|
|
2
|
+
//! `Microsandbox::Native::FsWriteSink`.
|
|
3
|
+
//!
|
|
4
|
+
//! Wraps the core `SandboxFs::read_stream`/`write_stream` handles so large files
|
|
5
|
+
//! can be moved without buffering the whole thing in memory. The core
|
|
6
|
+
//! `FsReadStream::recv` needs `&mut self` and `FsWriteSink::close` consumes
|
|
7
|
+
//! `self`, so each is held behind a `tokio::Mutex` (the sink as an `Option`, so
|
|
8
|
+
//! `close` can take it and be idempotent). Each call drives the future to
|
|
9
|
+
//! completion with the GVL released; the Ruby layer wraps these as an
|
|
10
|
+
//! `Enumerable` reader and a writer with a block form.
|
|
11
|
+
|
|
12
|
+
use std::sync::Arc;
|
|
13
|
+
|
|
14
|
+
use magnus::{method, prelude::*, Error, RModule, RString, Ruby};
|
|
15
|
+
use microsandbox::sandbox::{FsReadStream, FsWriteSink};
|
|
16
|
+
use tokio::sync::Mutex;
|
|
17
|
+
|
|
18
|
+
use crate::error;
|
|
19
|
+
use crate::runtime::{block_on, ruby};
|
|
20
|
+
|
|
21
|
+
#[magnus::wrap(class = "Microsandbox::Native::FsReadStream", free_immediately, size)]
|
|
22
|
+
pub struct FsReadStreamHandle {
|
|
23
|
+
inner: Arc<Mutex<FsReadStream>>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl FsReadStreamHandle {
|
|
27
|
+
pub fn new(stream: FsReadStream) -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
inner: Arc::new(Mutex::new(stream)),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Next chunk of bytes (ASCII-8BIT), or nil at end of stream.
|
|
34
|
+
fn recv(&self) -> Result<Option<RString>, Error> {
|
|
35
|
+
let inner = Arc::clone(&self.inner);
|
|
36
|
+
match block_on(async move { inner.lock().await.recv().await }).map_err(error::to_ruby)? {
|
|
37
|
+
Some(bytes) => Ok(Some(ruby().str_from_slice(bytes.as_ref()))),
|
|
38
|
+
None => Ok(None),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[magnus::wrap(class = "Microsandbox::Native::FsWriteSink", free_immediately, size)]
|
|
44
|
+
pub struct FsWriteSinkHandle {
|
|
45
|
+
inner: Arc<Mutex<Option<FsWriteSink>>>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl FsWriteSinkHandle {
|
|
49
|
+
pub fn new(sink: FsWriteSink) -> Self {
|
|
50
|
+
Self {
|
|
51
|
+
inner: Arc::new(Mutex::new(Some(sink))),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Write a chunk of bytes. Errors if the sink is already closed.
|
|
56
|
+
fn write(&self, data: RString) -> Result<(), Error> {
|
|
57
|
+
// Copy out while the GVL is held (GC.compact could move the buffer).
|
|
58
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
59
|
+
let inner = Arc::clone(&self.inner);
|
|
60
|
+
let result = block_on(async move {
|
|
61
|
+
let guard = inner.lock().await;
|
|
62
|
+
match guard.as_ref() {
|
|
63
|
+
Some(sink) => Some(sink.write(&bytes).await),
|
|
64
|
+
None => None,
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
match result {
|
|
68
|
+
Some(r) => r.map_err(error::to_ruby),
|
|
69
|
+
None => Err(error::base_error("write to a closed FsWriteSink")),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Flush and close the sink. Idempotent.
|
|
74
|
+
fn close(&self) -> Result<(), Error> {
|
|
75
|
+
let inner = Arc::clone(&self.inner);
|
|
76
|
+
let taken = block_on(async move { inner.lock().await.take() });
|
|
77
|
+
match taken {
|
|
78
|
+
Some(sink) => block_on(sink.close()).map_err(error::to_ruby),
|
|
79
|
+
None => Ok(()),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
85
|
+
let rs = native.define_class("FsReadStream", ruby.class_object())?;
|
|
86
|
+
rs.define_method("recv", method!(FsReadStreamHandle::recv, 0))?;
|
|
87
|
+
|
|
88
|
+
let ws = native.define_class("FsWriteSink", ruby.class_object())?;
|
|
89
|
+
ws.define_method("write", method!(FsWriteSinkHandle::write, 1))?;
|
|
90
|
+
ws.define_method("close", method!(FsWriteSinkHandle::close, 0))?;
|
|
91
|
+
Ok(())
|
|
92
|
+
}
|
|
@@ -9,6 +9,7 @@ use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
|
|
|
9
9
|
use microsandbox::image::{Image, ImageDetail, ImageHandle, ImagePruneReport};
|
|
10
10
|
|
|
11
11
|
use crate::backend::with_local_backend;
|
|
12
|
+
use crate::conv;
|
|
12
13
|
use crate::runtime::ruby;
|
|
13
14
|
|
|
14
15
|
fn handle_to_hash(h: &ImageHandle) -> RHash {
|
|
@@ -43,6 +44,11 @@ fn detail_to_hash(detail: ImageDetail) -> RHash {
|
|
|
43
44
|
let _ = c.aset("entrypoint", config.entrypoint);
|
|
44
45
|
let _ = c.aset("working_dir", config.working_dir);
|
|
45
46
|
let _ = c.aset("user", config.user);
|
|
47
|
+
// OCI config labels: a free-form JSON object (or nil). Converted to a
|
|
48
|
+
// Ruby Hash so `ImageDetail#config["labels"]` matches the Python/Node
|
|
49
|
+
// `config.labels` dict. (The pinned v0.5.8 runtime persists this as
|
|
50
|
+
// null today; the key exists for forward-compatibility/parity.)
|
|
51
|
+
let _ = c.aset("labels", config.labels.as_ref().map(conv::json_to_ruby));
|
|
46
52
|
let _ = c.aset("stop_signal", config.stop_signal);
|
|
47
53
|
let _ = hash.aset("config", c);
|
|
48
54
|
} else {
|