microsandbox-rb 0.5.7
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 +7 -0
- data/CHANGELOG.md +94 -0
- data/Cargo.lock +7455 -0
- data/Cargo.toml +16 -0
- data/DESIGN.md +159 -0
- data/LICENSE +201 -0
- data/README.md +328 -0
- data/ext/microsandbox/Cargo.toml +45 -0
- data/ext/microsandbox/extconf.rb +14 -0
- data/ext/microsandbox/src/conv.rs +74 -0
- data/ext/microsandbox/src/error.rs +72 -0
- data/ext/microsandbox/src/exec.rs +158 -0
- data/ext/microsandbox/src/image.rs +114 -0
- data/ext/microsandbox/src/lib.rs +84 -0
- data/ext/microsandbox/src/runtime.rs +92 -0
- data/ext/microsandbox/src/sandbox.rs +812 -0
- data/ext/microsandbox/src/snapshot.rs +158 -0
- data/ext/microsandbox/src/stream.rs +86 -0
- data/ext/microsandbox/src/volume.rs +97 -0
- data/lib/microsandbox/errors.rb +68 -0
- data/lib/microsandbox/exec_handle.rb +154 -0
- data/lib/microsandbox/exec_output.rb +55 -0
- data/lib/microsandbox/fs.rb +172 -0
- data/lib/microsandbox/image.rb +111 -0
- data/lib/microsandbox/log_entry.rb +38 -0
- data/lib/microsandbox/metrics.rb +55 -0
- data/lib/microsandbox/sandbox.rb +461 -0
- data/lib/microsandbox/snapshot.rb +155 -0
- data/lib/microsandbox/streams.rb +54 -0
- data/lib/microsandbox/version.rb +7 -0
- data/lib/microsandbox/volume.rb +79 -0
- data/lib/microsandbox.rb +78 -0
- data/rust-toolchain.toml +5 -0
- data/sig/microsandbox.rbs +321 -0
- metadata +101 -0
data/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# microsandbox-rb
|
|
2
|
+
|
|
3
|
+
Lightweight microVM sandboxes for Ruby — run AI agents and untrusted code with hardware-level isolation.
|
|
4
|
+
|
|
5
|
+
The `microsandbox-rb` gem provides native bindings to the [microsandbox](https://github.com/superradcompany/microsandbox) runtime via a Rust extension (magnus). It spins up real microVMs (not containers) in under 100 ms, runs standard OCI (Docker) images, and gives you full control over command execution, the guest filesystem, networking, and metrics — all from an idiomatic, **synchronous** Ruby API. There is no daemon to install and no server to connect to: the runtime is embedded directly in your process.
|
|
6
|
+
|
|
7
|
+
This is an **unofficial, community-maintained** Ruby implementation — not part of the official SDK family ([Rust](https://github.com/superradcompany/microsandbox/tree/main/sdk), TypeScript, Python, Go) — though it wraps the same core engine.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Hardware isolation** — each sandbox is a real VM with its own Linux kernel
|
|
12
|
+
- **Sub-100 ms boot** — no daemon, no server setup, embedded directly in your app
|
|
13
|
+
- **OCI image support** — pull and run images from Docker Hub, GHCR, ECR, or any OCI registry
|
|
14
|
+
- **Command execution** — run commands or shell scripts and collect output
|
|
15
|
+
- **Guest filesystem access** — read, write, list, copy, stat files inside a running sandbox
|
|
16
|
+
- **Metrics & logs** — CPU, memory, disk and network I/O; captured stdout/stderr/system logs
|
|
17
|
+
- **Idiomatic Ruby** — keyword arguments, block-scoped lifecycle, a typed error hierarchy
|
|
18
|
+
- **Thread-friendly** — the GVL is released during sandbox calls, so other Ruby threads keep running
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- **Ruby** >= 3.1
|
|
23
|
+
- **Linux** with KVM enabled, or **macOS** on Apple Silicon (M-series)
|
|
24
|
+
- Building from source additionally needs a **Rust** toolchain (stable >= 1.91)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
The gem is published as **`microsandbox-rb`**, but you still `require "microsandbox"`
|
|
29
|
+
(the `microsandbox` package name was already taken on RubyGems):
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# Gemfile
|
|
33
|
+
gem "microsandbox-rb", require: "microsandbox"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bundle install
|
|
38
|
+
# or
|
|
39
|
+
gem install microsandbox-rb
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The first build downloads the `msb` runtime and `libkrunfw` firmware into
|
|
43
|
+
`~/.microsandbox`. You can (re)provision them explicitly at any time:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
Microsandbox.install unless Microsandbox.installed?
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
require "microsandbox"
|
|
53
|
+
|
|
54
|
+
Microsandbox::Sandbox.create("hello", image: "python") do |sb|
|
|
55
|
+
output = sb.exec("python", ["-c", "print('Hello, World!')"])
|
|
56
|
+
puts output.stdout # => "Hello, World!\n"
|
|
57
|
+
puts output.success? # => true
|
|
58
|
+
end
|
|
59
|
+
# the sandbox is stopped automatically when the block returns
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
### Lifecycle
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# Block form — recommended; stops the sandbox automatically (even on error)
|
|
68
|
+
Microsandbox::Sandbox.create("box", image: "alpine") do |sb|
|
|
69
|
+
# ...
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Manual form — you are responsible for stopping it
|
|
73
|
+
sb = Microsandbox::Sandbox.create("box", image: "alpine")
|
|
74
|
+
begin
|
|
75
|
+
# ...
|
|
76
|
+
ensure
|
|
77
|
+
sb.stop # graceful (sb.stop(timeout: 5) to bound the wait)
|
|
78
|
+
# sb.kill # force (SIGKILL)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Inspect / manage existing sandboxes
|
|
82
|
+
Microsandbox::Sandbox.list # => [Microsandbox::SandboxInfo, ...]
|
|
83
|
+
Microsandbox::Sandbox.get("box") # => Microsandbox::SandboxInfo
|
|
84
|
+
Microsandbox::Sandbox.start("box") # restart a stopped sandbox
|
|
85
|
+
Microsandbox::Sandbox.remove("box") # remove a stopped sandbox
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Configuration
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Microsandbox::Sandbox.create(
|
|
92
|
+
"configured",
|
|
93
|
+
image: "python",
|
|
94
|
+
cpus: 2,
|
|
95
|
+
memory: 1024, # MiB
|
|
96
|
+
env: { "API_BASE" => "https://example.com" },
|
|
97
|
+
workdir: "/app",
|
|
98
|
+
labels: { "team" => "research" },
|
|
99
|
+
ports: { 8080 => 80 }, # host => guest (TCP)
|
|
100
|
+
network: "public_only", # or "none" for airgapped
|
|
101
|
+
replace: true # replace an existing sandbox of the same name
|
|
102
|
+
) do |sb|
|
|
103
|
+
# ...
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Executing commands
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
Microsandbox::Sandbox.create("exec-demo", image: "alpine") do |sb|
|
|
111
|
+
# Direct command (no shell)
|
|
112
|
+
out = sb.exec("ls", ["-la", "/etc"], cwd: "/", timeout: 30)
|
|
113
|
+
out.exit_code # => 0
|
|
114
|
+
out.success? # => true
|
|
115
|
+
out.stdout # => "..." (UTF-8)
|
|
116
|
+
out.stderr_bytes # => raw ASCII-8BIT bytes
|
|
117
|
+
|
|
118
|
+
# Shell script (pipes, redirects, &&)
|
|
119
|
+
sb.shell("cat /etc/os-release | grep VERSION").stdout
|
|
120
|
+
|
|
121
|
+
# Environment, stdin, working directory
|
|
122
|
+
sb.exec("cat", [], stdin: "piped data")
|
|
123
|
+
sb.exec("sh", ["-c", "echo $GREETING"], env: { "GREETING" => "hi" })
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A non-zero exit is **not** an error — inspect `exit_code`/`success?`. Spawn-time
|
|
128
|
+
failures (e.g. command not found) and timeouts raise typed errors (see below).
|
|
129
|
+
|
|
130
|
+
### Guest filesystem
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
Microsandbox::Sandbox.create("fs-demo", image: "alpine") do |sb|
|
|
134
|
+
sb.fs.write("/tmp/data.txt", "hello")
|
|
135
|
+
sb.fs.read_text("/tmp/data.txt") # => "hello" (UTF-8)
|
|
136
|
+
sb.fs.read("/tmp/data.txt") # => raw bytes (ASCII-8BIT)
|
|
137
|
+
sb.fs.exists?("/tmp/data.txt") # => true
|
|
138
|
+
|
|
139
|
+
sb.fs.mkdir("/tmp/sub")
|
|
140
|
+
sb.fs.copy("/tmp/data.txt", "/tmp/sub/copy.txt")
|
|
141
|
+
sb.fs.rename("/tmp/sub/copy.txt", "/tmp/sub/renamed.txt")
|
|
142
|
+
sb.fs.list("/tmp/sub") # => [Microsandbox::FsEntry, ...]
|
|
143
|
+
sb.fs.stat("/tmp/data.txt") # => Microsandbox::FsMetadata
|
|
144
|
+
|
|
145
|
+
# Host <-> guest copies
|
|
146
|
+
sb.fs.copy_from_host("./local.txt", "/tmp/local.txt")
|
|
147
|
+
sb.fs.copy_to_host("/tmp/out.txt", "./out.txt")
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Metrics & logs
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
Microsandbox::Sandbox.create("obs", image: "alpine") do |sb|
|
|
155
|
+
m = sb.metrics # => Microsandbox::Metrics
|
|
156
|
+
m.cpu_percent
|
|
157
|
+
m.memory_bytes
|
|
158
|
+
m.uptime_secs
|
|
159
|
+
|
|
160
|
+
sb.logs(tail: 100, sources: ["stdout", "stderr"]).each do |entry|
|
|
161
|
+
puts "[#{entry.source}] #{entry.text}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Streaming output
|
|
167
|
+
|
|
168
|
+
For long-running commands, stream events as they arrive instead of waiting:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
Microsandbox::Sandbox.create("stream", image: "python") do |sb|
|
|
172
|
+
handle = sb.exec_stream("python", ["-u", "-c", "import time\nfor i in range(3): print(i); time.sleep(1)"])
|
|
173
|
+
handle.each do |event| # ExecHandle is Enumerable
|
|
174
|
+
print event.text if event.stdout?
|
|
175
|
+
end
|
|
176
|
+
# or: out = handle.collect → ExecOutput (drain to the end)
|
|
177
|
+
# interactive stdin:
|
|
178
|
+
# sink = handle.stdin; sink.write("data\n"); sink.close
|
|
179
|
+
# control: handle.signal(15), handle.kill, handle.resize(rows, cols)
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Images
|
|
184
|
+
|
|
185
|
+
Manage the local OCI image cache (images are pulled automatically on `create`):
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
Microsandbox::Image.list # => [Microsandbox::ImageInfo, ...]
|
|
189
|
+
Microsandbox::Image.get("alpine") # => Microsandbox::ImageInfo
|
|
190
|
+
Microsandbox::Image.inspect("alpine").layers # => [{...}, ...]
|
|
191
|
+
Microsandbox::Image.remove("alpine", force: true)
|
|
192
|
+
report = Microsandbox::Image.prune
|
|
193
|
+
report.bytes_reclaimed
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Named volumes
|
|
197
|
+
|
|
198
|
+
Persistent storage that outlives individual sandboxes:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
Microsandbox::Volume.create("cache", kind: "disk", size_mib: 512)
|
|
202
|
+
Microsandbox::Volume.list # => [Microsandbox::VolumeInfo, ...]
|
|
203
|
+
|
|
204
|
+
Microsandbox::Sandbox.create("with-vol", image: "alpine",
|
|
205
|
+
volumes: { "/data" => { named: "cache" } }) do |sb|
|
|
206
|
+
sb.fs.write("/data/state.txt", "persisted")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
Microsandbox::Volume.remove("cache")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`volumes:` accepts a host path String (bind mount) or `{ bind: "/host" }` /
|
|
213
|
+
`{ named: "volume-name" }` per guest path. Boot from a snapshot with
|
|
214
|
+
`Sandbox.create(name, from_snapshot: "snap-name-or-path")`.
|
|
215
|
+
|
|
216
|
+
### Error handling
|
|
217
|
+
|
|
218
|
+
All errors descend from `Microsandbox::Error` and carry a stable `#code`:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
begin
|
|
222
|
+
Microsandbox::Sandbox.create("dup", image: "alpine")
|
|
223
|
+
Microsandbox::Sandbox.create("dup", image: "alpine") # name clash
|
|
224
|
+
rescue Microsandbox::SandboxAlreadyExistsError => e
|
|
225
|
+
warn "#{e.code}: #{e.message}" # => "sandbox-already-exists: ..."
|
|
226
|
+
rescue Microsandbox::Error => e
|
|
227
|
+
warn "microsandbox failed: #{e.message}"
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Class | `#code` |
|
|
232
|
+
|-------|---------|
|
|
233
|
+
| `InvalidConfigError` | `invalid-config` |
|
|
234
|
+
| `SandboxNotFoundError` | `sandbox-not-found` |
|
|
235
|
+
| `SandboxAlreadyExistsError` | `sandbox-already-exists` |
|
|
236
|
+
| `SandboxStillRunningError` | `sandbox-still-running` |
|
|
237
|
+
| `ExecTimeoutError` | `exec-timeout` |
|
|
238
|
+
| `ExecFailedError` | `exec-failed` |
|
|
239
|
+
| `FilesystemError` | `filesystem-error` |
|
|
240
|
+
| `ImageNotFoundError` | `image-not-found` |
|
|
241
|
+
| `MetricsDisabledError` / `MetricsUnavailableError` | `metrics-disabled` / `metrics-unavailable` |
|
|
242
|
+
| … | (see `lib/microsandbox/errors.rb`) |
|
|
243
|
+
|
|
244
|
+
## Runtime configuration
|
|
245
|
+
|
|
246
|
+
The `msb` runtime path is resolved in this order: the `MSB_PATH` environment
|
|
247
|
+
variable → an SDK-set override → the config file → `~/.microsandbox/bin/msb` →
|
|
248
|
+
`msb` on `PATH`.
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
Microsandbox.installed? # => true/false
|
|
252
|
+
Microsandbox.install # download + install the runtime (idempotent)
|
|
253
|
+
Microsandbox.runtime_path # => "/Users/you/.microsandbox/bin/msb"
|
|
254
|
+
Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Development
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
bin/setup # bundle install
|
|
261
|
+
bundle exec rake compile # build the native extension (debug)
|
|
262
|
+
bundle exec rake compile:release # build optimized
|
|
263
|
+
bundle exec rake spec # run unit specs (no runtime needed)
|
|
264
|
+
MICROSANDBOX_INTEGRATION=1 bundle exec rake spec:all # + real microVM specs
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Unit specs run without a runtime. Integration specs boot real microVMs and are
|
|
268
|
+
opt-in via `MICROSANDBOX_INTEGRATION=1` (override the test image with
|
|
269
|
+
`MICROSANDBOX_TEST_IMAGE`).
|
|
270
|
+
|
|
271
|
+
The native extension depends on the `microsandbox` core crate via a pinned git
|
|
272
|
+
tag, so it builds without an adjacent checkout. To develop against a sibling
|
|
273
|
+
`microsandbox/` checkout instead (faster, reflects local runtime changes):
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
cp .cargo/config.toml.example .cargo/config.toml # gitignored path override
|
|
277
|
+
bundle exec rake compile
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Releasing
|
|
281
|
+
|
|
282
|
+
Releases are automated by `.github/workflows/release.yml` via RubyGems
|
|
283
|
+
**Trusted Publishing** (OIDC) — there is no API key to store as a secret.
|
|
284
|
+
|
|
285
|
+
**One-time setup** (before the first release), create a *pending* trusted
|
|
286
|
+
publisher at <https://rubygems.org/profile/oidc/pending_trusted_publishers>:
|
|
287
|
+
|
|
288
|
+
| Field | Value |
|
|
289
|
+
|-------|-------|
|
|
290
|
+
| RubyGems gem name | `microsandbox-rb` |
|
|
291
|
+
| Repository owner | `ya-luotao` |
|
|
292
|
+
| Repository name | `microsandbox-rb` |
|
|
293
|
+
| Workflow filename | `release.yml` |
|
|
294
|
+
| Environment | *(leave blank)* |
|
|
295
|
+
|
|
296
|
+
On the first successful push the pending publisher auto-converts to a permanent
|
|
297
|
+
one bound to the gem.
|
|
298
|
+
|
|
299
|
+
**Each release:**
|
|
300
|
+
|
|
301
|
+
1. Bump `Microsandbox::VERSION` (and the `tag = "vX.Y.Z"` on the core-crate
|
|
302
|
+
dependency in `ext/microsandbox/Cargo.toml`) to match the upstream runtime,
|
|
303
|
+
update `CHANGELOG.md`.
|
|
304
|
+
2. *(Recommended first time)* run the workflow's manual `dry_run` dispatch to
|
|
305
|
+
build all platform gems without publishing — confirms the `arm64-darwin`
|
|
306
|
+
cross-build succeeds.
|
|
307
|
+
3. Push a `vX.Y.Z` tag. CI builds precompiled, multi-ABI platform gems
|
|
308
|
+
(`x86_64-linux`, `aarch64-linux`, `arm64-darwin`) with `rake-compiler-dock`,
|
|
309
|
+
plus the source gem, and pushes them to RubyGems. The publish job uses
|
|
310
|
+
`rubygems/configure-rubygems-credentials` (OIDC) with `id-token: write` — no
|
|
311
|
+
`RUBYGEMS_API_KEY` secret required.
|
|
312
|
+
|
|
313
|
+
See [DESIGN.md](DESIGN.md) for the architecture and the implemented-surface
|
|
314
|
+
section for what's covered today vs. on the roadmap. Covered: full sandbox
|
|
315
|
+
lifecycle (including the async `request_stop`/`request_kill`/`request_drain`/
|
|
316
|
+
`wait_until_stopped`/`detach`/`owns_lifecycle?` controls and label-filtered
|
|
317
|
+
`list_with`), `exec`/`shell` (collected and streaming), the full guest
|
|
318
|
+
filesystem, metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
|
|
319
|
+
streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
|
|
320
|
+
named volumes, and snapshots (create/list/verify/export/import +
|
|
321
|
+
boot-from-snapshot). Create options span resources, network policy presets,
|
|
322
|
+
`log_level`/`security`/`rlimits`/`pull_policy`/`secrets` and more; `exec`/`shell`
|
|
323
|
+
take per-call `rlimits`. Still on the roadmap: custom per-rule network policies,
|
|
324
|
+
file patches, registry auth, interactive `attach`, SSH, and the raw agent client.
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
Apache-2.0. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
# Distinct from the core `microsandbox` package: Cargo refuses two packages with
|
|
3
|
+
# the same name in one lockfile. The native artifact is loaded internally as
|
|
4
|
+
# `microsandbox/microsandbox_rb` and stays hidden behind `require "microsandbox"`.
|
|
5
|
+
name = "microsandbox_rb"
|
|
6
|
+
description = "Ruby SDK native extension for microsandbox — secure, fast microVM-based sandboxing."
|
|
7
|
+
version = "0.5.7"
|
|
8
|
+
authors = ["Super Rad Company <development@superrad.company>"]
|
|
9
|
+
repository = "https://github.com/superradcompany/microsandbox"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
edition = "2021"
|
|
12
|
+
publish = false
|
|
13
|
+
|
|
14
|
+
[lib]
|
|
15
|
+
# Produces libmicrosandbox_rb.{dylib,so}; rb-sys renames it to
|
|
16
|
+
# microsandbox/microsandbox_rb.{bundle,so}. The magnus #[init] entrypoint is
|
|
17
|
+
# therefore Init_microsandbox_rb (matches the loaded basename).
|
|
18
|
+
name = "microsandbox_rb"
|
|
19
|
+
crate-type = ["cdylib"]
|
|
20
|
+
|
|
21
|
+
[dependencies]
|
|
22
|
+
magnus = "0.8"
|
|
23
|
+
# Low-level Ruby C API — used directly for rb_thread_call_without_gvl (GVL release
|
|
24
|
+
# during blocking sandbox calls). Kept in lock-step with magnus's own rb-sys.
|
|
25
|
+
rb-sys = "0.9"
|
|
26
|
+
|
|
27
|
+
# The microsandbox core runtime, wrapped via FFI exactly like the official
|
|
28
|
+
# Python (pyo3) and Node (napi) SDKs. Pinned to the matching upstream tag so the
|
|
29
|
+
# gem is self-contained and buildable anywhere (CI, release containers, end-user
|
|
30
|
+
# source installs) without an adjacent checkout. For local development against a
|
|
31
|
+
# sibling checkout, use the `paths` override in `.cargo/config.toml` (see
|
|
32
|
+
# `.cargo/config.toml.example`). "ssh" matches the feature set the Python/Node
|
|
33
|
+
# SDKs ship with; default features add "prebuilt" (provisions msb + libkrunfw at
|
|
34
|
+
# build time), "net", and "keyring".
|
|
35
|
+
microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.7", default-features = true, features = ["ssh"] }
|
|
36
|
+
microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.7" }
|
|
37
|
+
|
|
38
|
+
# Async core bridged to Ruby's synchronous API via a blocking tokio runtime.
|
|
39
|
+
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
|
|
40
|
+
futures = "0.3"
|
|
41
|
+
|
|
42
|
+
# Shared (de)serialization + value types used across the binding surface.
|
|
43
|
+
serde_json = "1"
|
|
44
|
+
chrono = "0.4"
|
|
45
|
+
ipnetwork = { version = "0.21", features = ["serde"] }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "rb_sys/mkmf"
|
|
5
|
+
|
|
6
|
+
# Builds the Rust cdylib and installs it as lib/microsandbox/microsandbox.{bundle,so}.
|
|
7
|
+
# The Cargo profile is selected via the RB_SYS_CARGO_PROFILE env var (defaults to
|
|
8
|
+
# release for installed gems, dev for `rake compile`).
|
|
9
|
+
#
|
|
10
|
+
# Cargo features for the embedded microsandbox runtime (e.g. "ssh") are enabled
|
|
11
|
+
# directly on the dependency in ext/microsandbox/Cargo.toml, mirroring the official
|
|
12
|
+
# Python/Node SDKs. The crate's "prebuilt" default feature provisions the msb
|
|
13
|
+
# runtime + libkrunfw into ~/.microsandbox at build time.
|
|
14
|
+
create_rust_makefile("microsandbox/microsandbox_rb")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//! Helpers for reading a Ruby options `Hash` (string keys) into Rust values.
|
|
2
|
+
//!
|
|
3
|
+
//! The Ruby layer normalizes keyword arguments into a string-keyed Hash before
|
|
4
|
+
//! handing it to the native layer, so lookups here are by `&str`.
|
|
5
|
+
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
|
|
8
|
+
use magnus::{value::ReprValue, Error, RHash, TryConvert, Value};
|
|
9
|
+
|
|
10
|
+
/// Fetch a non-nil value for `key`, if present.
|
|
11
|
+
fn get(hash: RHash, key: &str) -> Option<Value> {
|
|
12
|
+
match hash.get(key) {
|
|
13
|
+
Some(v) if !v.is_nil() => Some(v),
|
|
14
|
+
_ => None,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Generic typed fetch: `None` if the key is absent/nil, else converted.
|
|
19
|
+
pub fn opt<T: TryConvert>(hash: RHash, key: &str) -> Result<Option<T>, Error> {
|
|
20
|
+
match get(hash, key) {
|
|
21
|
+
Some(v) => Ok(Some(T::try_convert(v)?)),
|
|
22
|
+
None => Ok(None),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn opt_string(hash: RHash, key: &str) -> Result<Option<String>, Error> {
|
|
27
|
+
opt(hash, key)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn opt_bool(hash: RHash, key: &str) -> Result<bool, Error> {
|
|
31
|
+
Ok(opt::<bool>(hash, key)?.unwrap_or(false))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn opt_u8(hash: RHash, key: &str) -> Result<Option<u8>, Error> {
|
|
35
|
+
opt(hash, key)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pub fn opt_u32(hash: RHash, key: &str) -> Result<Option<u32>, Error> {
|
|
39
|
+
opt(hash, key)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn opt_f64(hash: RHash, key: &str) -> Result<Option<f64>, Error> {
|
|
43
|
+
opt(hash, key)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// String→String map (e.g. `env`, `labels`, `scripts`). Empty if absent.
|
|
47
|
+
pub fn opt_string_map(hash: RHash, key: &str) -> Result<Vec<(String, String)>, Error> {
|
|
48
|
+
match get(hash, key) {
|
|
49
|
+
Some(v) => {
|
|
50
|
+
let map = HashMap::<String, String>::try_convert(v)?;
|
|
51
|
+
Ok(map.into_iter().collect())
|
|
52
|
+
}
|
|
53
|
+
None => Ok(Vec::new()),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Array of strings (e.g. `entrypoint`, `args`). Empty if absent.
|
|
58
|
+
pub fn opt_string_vec(hash: RHash, key: &str) -> Result<Vec<String>, Error> {
|
|
59
|
+
match get(hash, key) {
|
|
60
|
+
Some(v) => Ok(Vec::<String>::try_convert(v)?),
|
|
61
|
+
None => Ok(Vec::new()),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// `u16`→`u16` port map (host→guest TCP). Empty if absent.
|
|
66
|
+
pub fn opt_port_map(hash: RHash, key: &str) -> Result<Vec<(u16, u16)>, Error> {
|
|
67
|
+
match get(hash, key) {
|
|
68
|
+
Some(v) => {
|
|
69
|
+
let map = HashMap::<u16, u16>::try_convert(v)?;
|
|
70
|
+
Ok(map.into_iter().collect())
|
|
71
|
+
}
|
|
72
|
+
None => Ok(Vec::new()),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//! Map the core `MicrosandboxError` enum onto the Ruby exception hierarchy.
|
|
2
|
+
//!
|
|
3
|
+
//! Mirrors `sdk/python/src/error.rs`: each handled variant is routed to a
|
|
4
|
+
//! specific `Microsandbox::*Error` class (defined in `lib/microsandbox/errors.rb`),
|
|
5
|
+
//! every other variant falls back to the base `Microsandbox::Error`. The message
|
|
6
|
+
//! is always the core error's `to_string()`.
|
|
7
|
+
|
|
8
|
+
use magnus::{value::ReprValue, Error, ExceptionClass, Module, RClass, RModule, Ruby};
|
|
9
|
+
use microsandbox::{AgentClientError, MicrosandboxError};
|
|
10
|
+
|
|
11
|
+
/// The Ruby class (relative to the `Microsandbox` module) for a core error.
|
|
12
|
+
/// `"Error"` is the base class; anything else is a named subclass.
|
|
13
|
+
fn class_name(err: &MicrosandboxError) -> &'static str {
|
|
14
|
+
use MicrosandboxError::*;
|
|
15
|
+
match err {
|
|
16
|
+
InvalidConfig(_) => "InvalidConfigError",
|
|
17
|
+
SandboxNotFound(_) => "SandboxNotFoundError",
|
|
18
|
+
SandboxAlreadyExists(_) => "SandboxAlreadyExistsError",
|
|
19
|
+
SandboxStillRunning(_) => "SandboxStillRunningError",
|
|
20
|
+
ExecTimeout(_) => "ExecTimeoutError",
|
|
21
|
+
ExecFailed(_) => "ExecFailedError",
|
|
22
|
+
SandboxFsOps(_) => "FilesystemError",
|
|
23
|
+
ImageNotFound(_) => "ImageNotFoundError",
|
|
24
|
+
ImageInUse(_) => "ImageInUseError",
|
|
25
|
+
VolumeNotFound(_) => "VolumeNotFoundError",
|
|
26
|
+
VolumeAlreadyExists(_) => "VolumeAlreadyExistsError",
|
|
27
|
+
Io(_) => "IoError",
|
|
28
|
+
MetricsDisabled(_) => "MetricsDisabledError",
|
|
29
|
+
MetricsUnavailable(_) => "MetricsUnavailableError",
|
|
30
|
+
AgentClient(AgentClientError::UnsupportedOperation { .. }) => "UnsupportedOperationError",
|
|
31
|
+
_ => "Error",
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Look up `Microsandbox::<name>` as an exception class.
|
|
36
|
+
fn exception_class(ruby: &Ruby, name: &str) -> Option<ExceptionClass> {
|
|
37
|
+
let module: RModule = ruby.class_object().const_get("Microsandbox").ok()?;
|
|
38
|
+
let class: RClass = module.const_get(name).ok()?;
|
|
39
|
+
ExceptionClass::from_value(class.as_value())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Convert a core error into a Ruby exception, preserving the typed class.
|
|
43
|
+
// The `exception::runtime_error()` fallbacks fire only off a Ruby thread (which
|
|
44
|
+
// never happens from a bound method); there is no handle-based alternative there.
|
|
45
|
+
#[allow(deprecated)]
|
|
46
|
+
pub fn to_ruby(err: MicrosandboxError) -> Error {
|
|
47
|
+
let message = err.to_string();
|
|
48
|
+
let ruby = match Ruby::get() {
|
|
49
|
+
Ok(ruby) => ruby,
|
|
50
|
+
// Not on a Ruby thread (should never happen from a bound method).
|
|
51
|
+
Err(_) => return Error::new(magnus::exception::runtime_error(), message),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
match exception_class(&ruby, class_name(&err)) {
|
|
55
|
+
Some(class) => Error::new(class, message),
|
|
56
|
+
None => Error::new(ruby.exception_runtime_error(), message),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// A plain `Microsandbox::Error` (base) with a custom message — used for
|
|
61
|
+
/// binding-level validation errors that have no core counterpart.
|
|
62
|
+
#[allow(deprecated)]
|
|
63
|
+
pub fn base_error(message: impl Into<String>) -> Error {
|
|
64
|
+
let message = message.into();
|
|
65
|
+
match Ruby::get() {
|
|
66
|
+
Ok(ruby) => match exception_class(&ruby, "Error") {
|
|
67
|
+
Some(class) => Error::new(class, message),
|
|
68
|
+
None => Error::new(ruby.exception_runtime_error(), message),
|
|
69
|
+
},
|
|
70
|
+
Err(_) => Error::new(magnus::exception::runtime_error(), message),
|
|
71
|
+
}
|
|
72
|
+
}
|