kobako 0.29.0 → 0.30.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +217 -10
- data/Cargo.lock +139 -141
- data/README.md +96 -574
- data/ROADMAP.md +2 -1
- data/crates/kobako-runtime/CHANGELOG.md +269 -1
- data/crates/kobako-runtime/Cargo.toml +25 -15
- data/crates/kobako-runtime/README.md +7 -3
- data/crates/kobako-runtime/src/dispatch.rs +1 -1
- data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
- data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
- data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
- data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
- data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
- data/crates/kobako-runtime/src/endpoint.rs +166 -0
- data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
- data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
- data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
- data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
- data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
- data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
- data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
- data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
- data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
- data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
- data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
- data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
- data/crates/kobako-runtime/src/filesystem.rs +21 -387
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +11 -6
- data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
- data/crates/kobako-runtime/src/locking.rs +218 -0
- data/crates/kobako-runtime/src/mount.rs +31 -104
- data/crates/kobako-runtime/src/profile.rs +137 -1
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
- data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
- data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
- data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
- data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
- data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
- data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
- data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
- data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
- data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
- data/crates/kobako-runtime/src/vfs/path.rs +64 -0
- data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
- data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
- data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
- data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
- data/crates/kobako-runtime/src/vfs.rs +60 -0
- data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
- data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
- data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
- data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
- data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
- data/crates/kobako-transport/CHANGELOG.md +8 -2
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
- data/crates/kobako-transport/src/envelope/call.rs +5 -5
- data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
- data/crates/kobako-transport/src/envelope/fault.rs +2 -2
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/request.rs +2 -2
- data/crates/kobako-transport/src/envelope/run.rs +1 -1
- data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
- data/crates/kobako-transport/src/envelope.rs +2 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
- data/crates/kobako-wasmtime/Cargo.toml +15 -14
- data/crates/kobako-wasmtime/README.md +9 -5
- data/crates/kobako-wasmtime/src/abi.rs +12 -20
- data/crates/kobako-wasmtime/src/ambient.rs +1 -2
- data/crates/kobako-wasmtime/src/cache.rs +5 -5
- data/crates/kobako-wasmtime/src/config.rs +22 -7
- data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
- data/crates/kobako-wasmtime/src/driver.rs +29 -96
- data/crates/kobako-wasmtime/src/exports.rs +5 -16
- data/crates/kobako-wasmtime/src/guest.rs +38 -0
- data/crates/kobako-wasmtime/src/imports.rs +14 -8
- data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
- data/crates/kobako-wasmtime/src/invocation.rs +26 -46
- data/crates/kobako-wasmtime/src/lib.rs +1 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
- data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
- data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
- data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
- data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
- data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
- data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
- data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
- data/crates/kobako-wasmtime/src/preview1.rs +464 -413
- data/crates/kobako-wasmtime/src/trap.rs +9 -9
- data/crates/kobako-wasmtime/src/wasi.rs +53 -37
- data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
- data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
- data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/runtime/bridge.rs +1 -1
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +28 -14
- data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
- data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
- data/ext/kobako/src/runtime/filesystem.rs +109 -137
- data/ext/kobako/src/runtime/gvl.rs +43 -8
- data/ext/kobako/src/runtime/snapshot.rs +1 -1
- data/ext/kobako/src/runtime.rs +6 -1
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -2
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +2 -10
- data/lib/kobako/catalog/services.rb +2 -2
- data/lib/kobako/catalog/snippets.rb +7 -7
- data/lib/kobako/catalog.rb +1 -1
- data/lib/kobako/codec/decoder.rb +10 -10
- data/lib/kobako/codec/ext_types.rb +3 -5
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +3 -3
- data/lib/kobako/context.rb +5 -5
- data/lib/kobako/filesystem.rb +44 -13
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +4 -4
- data/lib/kobako/sandbox.rb +28 -28
- data/lib/kobako/sandbox_options.rb +2 -3
- data/lib/kobako/snippet/bytecode.rb +1 -1
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/transport/dispatcher.rb +5 -5
- data/lib/kobako/transport/exposure.rb +2 -2
- data/lib/kobako/transport/reflection.rb +7 -7
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +1 -1
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +34 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +0 -4
- data/sig/kobako/catalog/services.rbs +1 -1
- data/sig/kobako/catalog/snippets.rbs +3 -3
- data/sig/kobako/catalog.rbs +1 -1
- data/sig/kobako/codec/decoder.rbs +1 -1
- data/sig/kobako/codec/handle_walk.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +2 -2
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +1 -1
- data/sig/kobako/codec.rbs +1 -2
- data/sig/kobako/context.rbs +1 -1
- data/sig/kobako/filesystem.rbs +19 -14
- data/sig/kobako/guest_yielder.rbs +1 -1
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +2 -2
- data/sig/kobako/runtime.rbs +1 -1
- data/sig/kobako/sandbox.rbs +3 -5
- data/sig/kobako/sandbox_options.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +2 -2
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +2 -2
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport.rbs +1 -1
- metadata +67 -19
- data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
- data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
- data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
- data/crates/kobako-wasmtime/src/io.rs +0 -89
- data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
- /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
- /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
data/README.md
CHANGED
|
@@ -2,691 +2,213 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://deepwiki.com/elct9620/kobako)
|
|
4
4
|
|
|
5
|
-
Kobako is a Ruby gem that
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Kobako is a Ruby gem that runs untrusted mruby scripts in an in-process Wasm
|
|
6
|
+
sandbox: LLM-generated code, user formulas, student submissions, plugins. A
|
|
7
|
+
script reaches no host memory, file, network, or credential you did not hand it.
|
|
8
|
+
A Rust SDK offers the same sandbox to hosts in other languages.
|
|
8
9
|
|
|
9
10
|
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
│ Execution │ ◀─pipe─ │ puts / warn │
|
|
18
|
-
│ value │ ◀─last─ │ last expression │
|
|
19
|
-
│ stdout / stderr │ │ │
|
|
20
|
-
│ usage │ │ │
|
|
21
|
-
└──────────────────────┘ └──────────────────────┘
|
|
22
|
-
trusted untrusted
|
|
11
|
+
Host process Wasm guest
|
|
12
|
+
+---------------------+ +---------------------+
|
|
13
|
+
| Kobako::Sandbox |--eval-->| mruby interpreter |
|
|
14
|
+
| Services |<--call--| KV::Lookup.call(k) |
|
|
15
|
+
| Execution |<-result-| last expression |
|
|
16
|
+
+---------------------+ +---------------------+
|
|
17
|
+
trusted untrusted
|
|
23
18
|
```
|
|
24
19
|
|
|
25
20
|
## Requirements
|
|
26
21
|
|
|
22
|
+
The gem ships its Guest Binary and, on most platforms, a prebuilt native
|
|
23
|
+
extension.
|
|
24
|
+
|
|
27
25
|
| What | Which versions | When you need it |
|
|
28
26
|
|---|---|---|
|
|
29
|
-
| Ruby |
|
|
30
|
-
| Platform | Linux or
|
|
31
|
-
| Rust / Cargo | current stable | only where no prebuilt gem matches your platform
|
|
32
|
-
| WASI toolchain | vendored by the build | only when building
|
|
33
|
-
|
|
34
|
-
The precompiled `kobako.wasm` Guest Binary ships inside the gem, so end users do **not** need a WASI toolchain.
|
|
27
|
+
| Ruby | >= 3.3.0 | always |
|
|
28
|
+
| Platform | Linux, macOS, or Windows | always; Windows gaps are fixed last |
|
|
29
|
+
| Rust / Cargo | current stable | only where no prebuilt gem matches your platform |
|
|
30
|
+
| WASI toolchain | vendored by the build | only when building from a source checkout |
|
|
35
31
|
|
|
36
32
|
## Installation
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
Add the gem to your bundle.
|
|
39
35
|
|
|
40
36
|
```bash
|
|
41
37
|
bundle add kobako
|
|
42
|
-
# or
|
|
43
|
-
gem install kobako
|
|
44
38
|
```
|
|
45
39
|
|
|
46
40
|
## Quick Start
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
A Sandbox runs guest code and returns the record of that run.
|
|
49
43
|
|
|
50
44
|
```ruby
|
|
51
45
|
require "kobako"
|
|
52
46
|
|
|
53
47
|
sandbox = Kobako::Sandbox.new
|
|
54
|
-
sandbox.eval("1 + 2").value
|
|
48
|
+
sandbox.eval("1 + 2").value # => 3
|
|
55
49
|
```
|
|
56
50
|
|
|
57
51
|
```rust
|
|
58
52
|
use kobako::{Options, Sandbox};
|
|
59
53
|
|
|
60
|
-
// Options::default() is secure by default: the same caps as Ruby, hermetic isolation.
|
|
61
54
|
let sandbox = Sandbox::new("kobako.wasm", Options::default())?;
|
|
62
|
-
sandbox.eval("1 + 2")?.value()?;
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Each invocation returns the record of that one run: `Kobako::Execution` in Ruby, `Execution` in Rust. It carries the guest value alongside the run's output and resource usage. Nothing a run observes is stored on the Sandbox, so the same Sandbox serves concurrent invocations without them seeing each other.
|
|
66
|
-
|
|
67
|
-
The gem bundles its Guest Binary; a Rust host loads one explicitly — see [Frontends & Guest Binaries](#frontends--guest-binaries) for the packages and the Guest Binary variants.
|
|
68
|
-
|
|
69
|
-
## Frontends & Guest Binaries
|
|
70
|
-
|
|
71
|
-
Embedding kobako is two independent choices: the **host frontend** you build against, and the **Guest Binary** it runs.
|
|
72
|
-
|
|
73
|
-
```text
|
|
74
|
-
host frontend Guest Binary
|
|
75
|
-
Ruby gem ──┐ ┌── kobako.wasm (pure)
|
|
76
|
-
Rust SDK ──┼──┼── kobako+regexp.wasm
|
|
77
|
-
your own ──┘ └── kobako+json.wasm · your own
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
They compose freely, since any frontend loads any Guest Binary. A Ruby host can run a JSON-enabled guest, and a Rust host can run the pure default. How far down those choices go — and which ones a given starting point quietly makes for you — is laid out in [`docs/guides/assembly-levels.md`](docs/guides/assembly-levels.md).
|
|
81
|
-
|
|
82
|
-
### Host frontends
|
|
83
|
-
|
|
84
|
-
The host embeds the sandbox and owns the wire codec. Choose by your host language and how much of the contract you want pre-assembled.
|
|
85
|
-
|
|
86
|
-
| Frontend | Package | Add it | Best for |
|
|
87
|
-
|----------|---------|--------|----------|
|
|
88
|
-
| Ruby gem | `kobako` (RubyGems) | `gem install kobako` | A Ruby host — Services, Handles, snippets, and pooling out of the box |
|
|
89
|
-
| Rust SDK | `kobako` (crates.io) | `cargo add kobako` | A Rust host — the same behavior contract behind an idiomatic Rust API |
|
|
90
|
-
| Low-level crates | `kobako-wasmtime` + `kobako-runtime` + `kobako-transport` + `kobako-codec` | `cargo add` each | A custom host, or driving the wire directly in another language |
|
|
91
|
-
|
|
92
|
-
The Rust crates are documented on [crates.io](https://crates.io/crates/kobako); the Ruby gem is this README. Three runnable Rust hosts show the choice. [`plugin-rs`](examples/plugin-rs) builds on the SDK, and [`wire-rs`](examples/wire-rs) assembles a host by hand on the low-level crates. [`fixed-schema-rs`](examples/fixed-schema-rs) replaces the payload schema both of those keep, with a guest and a host that agree on protobuf.
|
|
93
|
-
|
|
94
|
-
### Pre-built Guest Binaries
|
|
95
|
-
|
|
96
|
-
The gem bundles the pure `kobako.wasm`. Regexp and JSON are opt-in capabilities compiled into separate variants, each attached to every [GitHub Release](https://github.com/elct9620/kobako/releases). Download a variant and point your host at it — `Sandbox.new(wasm_path:)` in Ruby, `Sandbox::new(path, ...)` in Rust. [`docs/guides/variants.md`](docs/guides/variants.md) lists every variant and what it adds.
|
|
97
|
-
|
|
98
|
-
```ruby
|
|
99
|
-
sandbox = Kobako::Sandbox.new(wasm_path: "kobako+full.wasm")
|
|
100
|
-
sandbox.eval('JSON.generate({ n: "42".to_i })').value # => "{\"n\":42}"
|
|
55
|
+
sandbox.eval("1 + 2")?.value()?; // => Value::Int(3)
|
|
101
56
|
```
|
|
102
57
|
|
|
103
|
-
|
|
58
|
+
These terms name what the rest of this README builds on.
|
|
104
59
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
| To decide | Read |
|
|
60
|
+
| Term | Meaning |
|
|
108
61
|
|---|---|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
113
|
-
|
|
62
|
+
| Sandbox | the unit you configure once and invoke many times |
|
|
63
|
+
| Invocation | one run of guest code, by `#eval` or `#run` |
|
|
64
|
+
| Execution | the frozen record an Invocation leaves |
|
|
65
|
+
| Service | a host object the guest reaches by name |
|
|
66
|
+
| Mount | a Filesystem placed at a guest path |
|
|
114
67
|
|
|
115
|
-
|
|
68
|
+
## Features
|
|
116
69
|
|
|
117
|
-
|
|
118
|
-
sandbox.flag(:strict_mode) # critical: refused where unrecognised
|
|
119
|
-
sandbox.flag(:compile_cache, "on", advisory: true) # skipped where unrecognised
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## Glossary
|
|
123
|
-
|
|
124
|
-
These terms name the concepts the rest of this README builds on.
|
|
125
|
-
|
|
126
|
-
| Term | Ruby class | Meaning |
|
|
127
|
-
|------|------------|---------|
|
|
128
|
-
| Sandbox | `Kobako::Sandbox` | The unit you configure once and invoke many times. |
|
|
129
|
-
| Invocation | — | One run of guest code, by `#eval` or `#run`. |
|
|
130
|
-
| Execution | `Kobako::Execution` | The frozen record an Invocation leaves. |
|
|
131
|
-
| Service | — | A host object the guest reaches by name, such as `MyService::KV`. |
|
|
132
|
-
| Mount | — | A `Kobako::Filesystem` placed at a guest path by `Sandbox#mount`, for the guest's `File`. |
|
|
133
|
-
| Context | `Kobako::Context` | The per-Invocation object the optional block receives. |
|
|
134
|
-
| Snippet | — | Guest source or bytecode replayed at the start of every Invocation. |
|
|
135
|
-
| Handle | `Kobako::Handle` | An opaque reference to a host object the wire cannot carry by value. |
|
|
136
|
-
| Block | — | Guest code passed alongside a Service call. |
|
|
137
|
-
|
|
138
|
-
A Sandbox keeps nothing from one Invocation to the next. An Execution holds what the run produced, wrote, and consumed: `#value`, output captures, and `#usage`. A failed run raises, carrying its Execution on the error's `#execution`. A Service or a Mount is the guest's only route to a host resource. A Context's `ctx.bind` supplies a Service object for that one run. A Handle names its object only within the Invocation that issued it. A Block stays in the guest; each `yield` to it is a synchronous round-trip.
|
|
70
|
+
Each feature below shows its smallest use; the [guides] hold the rest.
|
|
139
71
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
Each section below stands on its own; this is the order they build in.
|
|
143
|
-
|
|
144
|
-
| If you want to | Read |
|
|
145
|
-
|---|---|
|
|
146
|
-
| let guest code call into your host | [Services](#services), [Per-Invocation Bindings](#per-invocation-bindings), [Service Blocks](#service-blocks) |
|
|
147
|
-
| let guest code read and write files | [Mounts](#mounts) |
|
|
148
|
-
| see what a run produced or why it failed | [Output Capture](#output-capture), [Error Handling](#error-handling), [Invocation Lifecycle](#invocation-lifecycle) |
|
|
149
|
-
| bound what a run may consume | [Resource Limits](#resource-limits), [Concurrency](#concurrency), [Pooling](#pooling) |
|
|
150
|
-
| hand host objects across the boundary | [Handle Management](#handle-management) |
|
|
151
|
-
| ship guest code with the Sandbox | [Snippets & Entrypoints](#snippets--entrypoints), [Extensions](#extensions) |
|
|
72
|
+
[guides]: https://github.com/elct9620/kobako/tree/main/docs/guides
|
|
152
73
|
|
|
153
74
|
### Services
|
|
154
75
|
|
|
155
|
-
`bind`
|
|
156
|
-
|
|
157
|
-
```ruby
|
|
158
|
-
class User
|
|
159
|
-
attr_reader :name
|
|
160
|
-
|
|
161
|
-
def initialize(name:)
|
|
162
|
-
@name = name
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
sandbox.bind("Project::User", User.new(name: "alice"))
|
|
167
|
-
sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
|
|
168
|
-
|
|
169
|
-
sandbox.eval(<<~RUBY)
|
|
170
|
-
Project::User.name # => "alice"
|
|
171
|
-
KV::Lookup.call("user_42") # => "..."
|
|
172
|
-
RUBY
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Each `::`-separated path segment must match `/\A[A-Z]\w{0,65533}\z/`. A path under `Kobako`, which the Guest Binary keeps for itself, raises `ArgumentError`. Symbol kwargs travel transparently to the host method's keyword arguments. The registry seals at the first invocation (see [Invocation Lifecycle](#invocation-lifecycle)); later `#bind` raises `ArgumentError`.
|
|
176
|
-
|
|
177
|
-
### Per-Invocation Bindings
|
|
178
|
-
|
|
179
|
-
A setup-time `bind` fixes one object for the Sandbox's life. Some objects belong to a single run instead: the current request, the acting user, a per-tenant store. For those, declare the path at setup and fill it per invocation. `bind(path)` with no object reserves the name as a *fillable*: the guest sees the constant, while an unfilled dispatch fails closed as `Kobako::ServiceError`. The optional `#eval` / `#run` block fills it.
|
|
76
|
+
`bind` makes a host object reachable from the guest at a constant name.
|
|
180
77
|
|
|
181
78
|
```ruby
|
|
182
|
-
sandbox.bind("
|
|
183
|
-
|
|
184
|
-
sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
|
|
79
|
+
sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
|
|
80
|
+
sandbox.eval('KV::Lookup.call("user_42")').value
|
|
185
81
|
```
|
|
186
82
|
|
|
187
|
-
|
|
83
|
+
A Service can also be filled per Invocation, take a guest block, and hand back
|
|
84
|
+
host objects as opaque Handles.
|
|
188
85
|
|
|
189
86
|
### Mounts
|
|
190
87
|
|
|
191
|
-
`mount`
|
|
88
|
+
`mount` gives the guest's `File` and `Dir` a Filesystem at one path, read-only by default.
|
|
192
89
|
|
|
193
90
|
```ruby
|
|
194
|
-
sandbox.mount("/config", Kobako::Filesystem::
|
|
195
|
-
sandbox.
|
|
196
|
-
sandbox.mount("/data", Kobako::Filesystem::Directory.new("var/data")) { |path, _access| path.end_with?(".csv") }
|
|
197
|
-
|
|
198
|
-
sandbox.eval('File.read("/config/app.yml")')
|
|
91
|
+
sandbox.mount("/config", Kobako::Filesystem::Tmpfs.new("app.yml" => yaml))
|
|
92
|
+
sandbox.eval('File.read("/config/app.yml")').value
|
|
199
93
|
```
|
|
200
94
|
|
|
201
95
|
| Kind | Holds |
|
|
202
96
|
|---|---|
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `Overlay.new.layer(fs, access:)` | Layers stacked into one view, the first added on top |
|
|
207
|
-
|
|
208
|
-
A Memory or Temporary is bounded as a tmpfs mount is. Its files hold 64 MiB unless `limit:` gives another size in bytes. It holds one file or directory for each 4 KiB of that limit, and its root, unless `inodes:` gives another count. `nil` lifts either bound, and the guest raises `Errno::ENOSPC` past one.
|
|
209
|
-
|
|
210
|
-
A block decides each open and each change that reaches the Mount in the access's place, asked at most once for each. A falsy answer or a `StandardError` fails that call as `Errno::EACCES`. Mounts seal with the Sandbox like a `bind`, and `ctx.mount` replaces one for a single run.
|
|
211
|
-
|
|
212
|
-
#### Guest File
|
|
213
|
-
|
|
214
|
-
The guest's `File.open` takes a mode String such as `"r"`, `"w+"`, or `"wx"`, or an Integer of `File::Constants` flags, valued as CRuby's on Linux. An exclusive create lets Sandboxes sharing a directory agree on a lock file. The critical flag `disable_file` removes `File` from the guest altogether.
|
|
215
|
-
|
|
216
|
-
Beneath a mount, `File.open` and `IO.new` answer as CRuby does over a Linux directory. Where they differ, they only refuse more:
|
|
217
|
-
|
|
218
|
-
| When | The guest sees |
|
|
219
|
-
|---|---|
|
|
220
|
-
| nothing is mounted | `NotImplementedError` |
|
|
221
|
-
| a block refuses a change | `Errno::EACCES` where a read-only mount says `Errno::EROFS` |
|
|
222
|
-
| a Ruby Filesystem's file without `append` is asked to append | `Errno::ENOTSUP` on the write |
|
|
223
|
-
| an Integer mode holds a flag kobako does not honor, or `WRONLY` beside `RDWR` | `Errno::EINVAL` |
|
|
224
|
-
| a mode names an encoding, such as `"r:utf-8"` | `ArgumentError` |
|
|
225
|
-
| `IO.new` wraps stdout or stderr in a mode other than `"w"` | `ArgumentError` |
|
|
226
|
-
| `disable_file` is set | no `File` constant |
|
|
227
|
-
|
|
228
|
-
#### Overlays
|
|
229
|
-
|
|
230
|
-
`layer` adds a Filesystem beneath the Layers already there and answers a new Overlay. Each Layer is `:read`, the default, or `:read_write` on its own.
|
|
231
|
-
|
|
232
|
-
```ruby
|
|
233
|
-
overlay = Kobako::Filesystem::Overlay.new
|
|
234
|
-
.layer(Kobako::Filesystem::Temporary.new, access: :read_write) # empty every run
|
|
235
|
-
.layer(Kobako::Filesystem::Directory.new("app")) # never written
|
|
236
|
-
sandbox.mount("/app", overlay, access: :read_write)
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
A change to a file in a writable Layer stays there. A read-only Layer's file is first copied up to the nearest writable Layer above it, and a new or renamed file lands in the topmost one. A lower writable Layer's file does not move, raising `Errno::EXDEV`, as across devices. A removal hides what a lower Layer keeps with an [OCI whiteout](https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts), `.wh.<name>`, kept in the topmost writable Layer. Names starting with `.wh.`, in any case, are therefore reserved.
|
|
240
|
-
|
|
241
|
-
#### Filesystems in Ruby
|
|
242
|
-
|
|
243
|
-
Any object answering `open` and `stat` mounts as a Filesystem, directly or as a Layer. Its methods follow Ruby's `File`, `Dir`, and `IO`, so a real `::File` can answer as an open file.
|
|
244
|
-
|
|
245
|
-
| Method | Answers |
|
|
246
|
-
|---|---|
|
|
247
|
-
| `open(path, flags)` | an open file, below |
|
|
248
|
-
| `stat(path)` | an object answering `directory?`, `size`, and `ino`, as `File::Stat` does |
|
|
249
|
-
| `children(path)` | entry names, as `Dir.children` gives them |
|
|
250
|
-
| `mkdir` · `rmdir` · `unlink` · `rename(from, to, directory:)` | optional; without one, that change raises `Errno::EROFS` |
|
|
251
|
-
|
|
252
|
-
A `stat` may also answer `dev`; without one, the Filesystem stands for a single device. An `ino` names one directory while the guest holds it open, and names match exactly, as a block judges them. A path is relative to the mount, the empty path being its root, and `flags` are `File::Constants`. A `rename` told `directory: false` moves only a file and `true` only a directory; `nil` moves either. An `Errno` the guest has a class for reaches the script as itself, and any other `StandardError` as `Errno::EIO`. Every call re-enters Ruby, and its time counts toward `timeout:` without being interrupted.
|
|
253
|
-
|
|
254
|
-
##### Open Files
|
|
255
|
-
|
|
256
|
-
The file `open` answers takes the guest's reads and writes, each method named as `IO` names it.
|
|
257
|
-
|
|
258
|
-
| Method | Answers | Without it |
|
|
259
|
-
|---|---|---|
|
|
260
|
-
| `pread(length, offset)` · `size` | bytes, `nil` or `EOFError` at the end; the length | required |
|
|
261
|
-
| `pwrite(data, offset)` · `truncate(length)` | bytes written; nothing | that change raises `Errno::EROFS` |
|
|
262
|
-
| `append(data)` | where the file then ends, as `IO#pos` does | an append raises `Errno::ENOTSUP` |
|
|
263
|
-
| `fsync` · `close` | nothing | nothing to flush or release |
|
|
264
|
-
|
|
265
|
-
An `append` writes all of `data` at the end in one step, so another writer's append never lands over it.
|
|
97
|
+
| `Tmpfs` | files in memory |
|
|
98
|
+
| `Bind` | one host directory |
|
|
99
|
+
| `Overlay` | read-only Layers under a Tmpfs |
|
|
266
100
|
|
|
267
|
-
###
|
|
101
|
+
### Execution Records
|
|
268
102
|
|
|
269
|
-
|
|
103
|
+
An Execution carries the value, the output, and the usage. A failed run raises
|
|
104
|
+
one of three classes, each carrying its Execution.
|
|
270
105
|
|
|
271
106
|
```ruby
|
|
272
|
-
execution = sandbox.eval(
|
|
273
|
-
|
|
274
|
-
warn "be careful"
|
|
275
|
-
42
|
|
276
|
-
RUBY
|
|
277
|
-
|
|
278
|
-
execution.value # => 42
|
|
279
|
-
execution.stdout # => "hello\n"
|
|
280
|
-
execution.stderr # => "be careful\n"
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
### Error Handling
|
|
284
|
-
|
|
285
|
-
Every invocation either returns an Execution or raises exactly one of three classes, so you can route faults without inspecting messages. The full taxonomy lives in [`lib/kobako/errors.rb`](lib/kobako/errors.rb).
|
|
107
|
+
execution = sandbox.eval('puts "hi"; 42')
|
|
108
|
+
execution.stdout # => "hi\n"
|
|
286
109
|
|
|
287
|
-
```ruby
|
|
288
110
|
begin
|
|
289
111
|
sandbox.eval(script)
|
|
290
|
-
rescue Kobako::TrapError
|
|
291
|
-
|
|
292
|
-
rescue Kobako::ServiceError
|
|
293
|
-
# A host Service call failed and the script did not rescue it.
|
|
294
|
-
rescue Kobako::SandboxError => e
|
|
295
|
-
# The script raised, failed to compile, or returned an unrepresentable value,
|
|
296
|
-
# or the Guest Binary could not start the run.
|
|
297
|
-
logger.warn(e.execution.stderr) # the failed run's own captures and usage
|
|
112
|
+
rescue Kobako::TrapError, Kobako::ServiceError, Kobako::SandboxError => e
|
|
113
|
+
e.execution&.stderr
|
|
298
114
|
end
|
|
299
115
|
```
|
|
300
116
|
|
|
301
|
-
Each of these carries the failed run's Execution on `#execution`. A rescue reads that run's output and usage exactly as a successful caller reads them off the returned one. `#failed?` keeps the two apart when both `#value` are `nil` — a script whose last expression was `nil` versus one that never produced a value. A failure caught before the guest ran at all — a host pre-flight refusal — leaves `#execution` `nil`, since there is no run to report.
|
|
302
|
-
|
|
303
|
-
| Class | Parent | Trigger |
|
|
304
|
-
|---------------------------------|----------------|------------------------------------------------------|
|
|
305
|
-
| `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
|
|
306
|
-
| `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
|
|
307
|
-
| `Kobako::NoServiceError` | `ServiceError` | Nothing bound there, a dead Handle, or a method the guest may not call |
|
|
308
|
-
| `Kobako::ServiceArgumentError` | `ServiceError` | The call would not bind, or the Service raised `ArgumentError` |
|
|
309
|
-
| `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
|
|
310
|
-
| `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
|
|
311
|
-
| `Kobako::UnsupportedFlagError` | `SandboxError` | The Guest Binary does not recognise a critical `#flag`; no script ran |
|
|
312
|
-
| `Kobako::UndefinedEntrypointError` | `SandboxError` | `#run` named a constant no snippet defined; carries `#name` and `#available` |
|
|
313
|
-
|
|
314
|
-
`SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` fields when the guest produced a panic envelope.
|
|
315
|
-
|
|
316
|
-
#### Host Exceptions
|
|
317
|
-
|
|
318
|
-
A Service, a mount's block, or a Filesystem written in Ruby is your code running inside the run. What it raises splits by class.
|
|
319
|
-
|
|
320
|
-
| Raised by your code | Result |
|
|
321
|
-
|-----------------------------------------|-----------------------------------------------|
|
|
322
|
-
| a `StandardError` | the guest sees a failure it may rescue |
|
|
323
|
-
| `Interrupt`, `exit`, `throw`, `Timeout` | the run ends; the invocation raises it as is |
|
|
324
|
-
|
|
325
117
|
### Resource Limits
|
|
326
118
|
|
|
327
|
-
Each
|
|
328
|
-
|
|
329
|
-
```ruby
|
|
330
|
-
sandbox = Kobako::Sandbox.new(
|
|
331
|
-
timeout: 5.0, # seconds, default 60.0
|
|
332
|
-
setup_timeout: 2.0, # seconds, default 10.0
|
|
333
|
-
memory_limit: 10 * 1024 * 1024, # bytes, default 4 MiB
|
|
334
|
-
stdout_limit: 64 * 1024, # bytes, default 1 MiB
|
|
335
|
-
stderr_limit: 64 * 1024,
|
|
336
|
-
open_file_limit: 16 # descriptors, default 1024
|
|
337
|
-
)
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
| Cap | Raises | Default |
|
|
341
|
-
|-------------------|--------------------------------|---------|
|
|
342
|
-
| `timeout` | `Kobako::TimeoutError` | 60.0 s |
|
|
343
|
-
| `setup_timeout` | `Kobako::SetupError` at `new` | 10.0 s |
|
|
344
|
-
| `memory_limit` | `Kobako::MemoryLimitError` | 4 MiB |
|
|
345
|
-
| `stdout_limit` | output clipped (no raise) | 1 MiB |
|
|
346
|
-
| `stderr_limit` | output clipped (no raise) | 1 MiB |
|
|
347
|
-
| `open_file_limit` | `Errno::EMFILE` in the guest | 1024 |
|
|
348
|
-
|
|
349
|
-
`setup_timeout` bounds instantiating a Guest Binary and reading its ABI version, not compiling it. It applies the first time a path is loaded; later Sandboxes on that path skip the check. A Pool builds each Sandbox at its first checkout, so its `SetupError` raises from `with`. `memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget. `open_file_limit` counts every descriptor one invocation holds, the three standard ones and each Mount's included, so a guest cannot exhaust the host process's own.
|
|
350
|
-
|
|
351
|
-
#### Isolation Profile
|
|
352
|
-
|
|
353
|
-
Beyond the caps, `profile:` requests the Sandbox's isolation posture on a two-rung ladder.
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
:permissive < :hermetic (default)
|
|
357
|
-
live clock frozen clock
|
|
358
|
-
live entropy constant entropy
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
`:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources, an explicit trade of reproducibility. The guest reaches no ambient filesystem, environment, or network under either. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/guides/security.md`](docs/guides/security.md) § Isolation Profiles.
|
|
362
|
-
|
|
363
|
-
### Concurrency
|
|
364
|
-
|
|
365
|
-
A Sandbox keeps no state from any run, so concurrent Threads may invoke distinct Sandboxes or share a single one. Each invocation owns its Handles, captures, and usage either way. One Thread still runs one invocation at a time. Sharing a Sandbox adds a single obligation.
|
|
366
|
-
|
|
367
|
-
| What you bind | Who reaches it | What it owes |
|
|
368
|
-
|---|---|---|
|
|
369
|
-
| Bound once at setup | every Thread sharing the Sandbox | it must itself be thread-safe |
|
|
370
|
-
| Supplied per invocation — `ctx.bind`, or an Extension `provider:` | that invocation alone | nothing |
|
|
371
|
-
|
|
372
|
-
#### GVL Modes
|
|
373
|
-
|
|
374
|
-
By default an invocation holds Ruby's GVL for its whole span, so guest execution across Threads serializes. `gvl: :release` drops the GVL for the guest span and re-acquires it for each guest→host dispatch, running guest code in parallel across Threads.
|
|
375
|
-
|
|
376
|
-
```ruby
|
|
377
|
-
sandbox = Kobako::Sandbox.new(gvl: :release)
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
The mode is per-Sandbox and fixed at construction; it changes scheduling only, leaving isolation, Handle lifetimes, captures, and outcomes identical. `:hold` remains the default because releasing pays a handoff cost at every dispatch. Compute-bound scripts scale with Thread count, while dispatch-heavy ones match or trail `:hold`. `rake bench:gvl_scheduling` measures both ends on your own hardware.
|
|
381
|
-
|
|
382
|
-
### Invocation Lifecycle
|
|
383
|
-
|
|
384
|
-
One Sandbox serves many invocations. Service bindings and preloaded snippets persist across calls; everything a run produces — Handles, captured output, memory delta — belongs to that run alone.
|
|
385
|
-
|
|
386
|
-
```
|
|
387
|
-
───────────── setup phase (mutable) ─────────────
|
|
388
|
-
|
|
389
|
-
sandbox = Kobako::Sandbox.new
|
|
390
|
-
sandbox.bind("KV::Lookup", ...)
|
|
391
|
-
sandbox.bind("Req::Current") # fillable: object comes later
|
|
392
|
-
sandbox.preload(code: ..., name: :Adder)
|
|
393
|
-
sandbox.preload(code: ..., name: :Greeter)
|
|
394
|
-
|
|
395
|
-
│
|
|
396
|
-
▼
|
|
397
|
-
|
|
398
|
-
═════════════════ seal point ═════════════════
|
|
399
|
-
First #eval or #run freezes the Service registry
|
|
400
|
-
and snippet table. Further bind / preload now
|
|
401
|
-
raise ArgumentError.
|
|
402
|
-
|
|
403
|
-
│
|
|
404
|
-
▼
|
|
405
|
-
|
|
406
|
-
──────────────── invocation N ───────────────────
|
|
407
|
-
|
|
408
|
-
1. resolve this run's bindings, before the guest starts:
|
|
409
|
-
ctx.bind("Req::Current", ...) fills the fillable
|
|
410
|
-
provider: backends yield this run's object
|
|
411
|
-
|
|
412
|
-
2. start from the canonical boot state
|
|
413
|
-
(mruby pre-initialized into the artifact at build time)
|
|
414
|
-
|
|
415
|
-
3. replay snippets (in insertion order):
|
|
416
|
-
:Adder → defines Adder
|
|
417
|
-
:Greeter → defines Greeter
|
|
418
|
-
|
|
419
|
-
4. dispatch: eval(source) or run(:Target, *args, **kwargs)
|
|
420
|
-
|
|
421
|
-
5. return the run's Execution to host
|
|
422
|
-
|
|
423
|
-
6. discard the instance; per-invocation state ends with it:
|
|
424
|
-
· Handles invalidated
|
|
425
|
-
· captures frozen into the Execution
|
|
426
|
-
· memory delta zeroed
|
|
427
|
-
|
|
428
|
-
Services + snippets persist; invocation N+1 repeats.
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
For workloads that must be isolated from each other, construct a fresh `Kobako::Sandbox` per scope: per tenant, per student submission, per agent session. wasmtime's Engine and the compiled Module are cached at process scope, so additional Sandboxes amortize cold-start cost automatically.
|
|
432
|
-
|
|
433
|
-
### Pooling
|
|
434
|
-
|
|
435
|
-
For hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes. It hands each one to a single exclusive holder at a time. Construction forwards every `Sandbox.new` keyword verbatim; the optional block is the per-Sandbox setup window and runs exactly once per constructed Sandbox.
|
|
119
|
+
Each Invocation runs under caps, and reaching one ends that run alone.
|
|
436
120
|
|
|
437
121
|
```ruby
|
|
438
|
-
|
|
439
|
-
sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))).value }
|
|
122
|
+
Kobako::Sandbox.new(timeout: 5.0, memory_limit: 10 * 1024 * 1024)
|
|
443
123
|
```
|
|
444
124
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
| Option | Meaning | Default |
|
|
448
|
-
|--------|---------|---------|
|
|
449
|
-
| `slots:` | Upper bound on constructed Sandboxes | required |
|
|
450
|
-
| `checkout_timeout:` | Seconds `#with` waits for a free Sandbox; `nil` waits indefinitely | 5.0 |
|
|
451
|
-
|
|
452
|
-
Sandboxes construct lazily on first demand. `#with` yields a Sandbox and returns the block's value, and at block exit the Sandbox returns to the pool, whatever the block raised. A checkout that waits past `checkout_timeout` raises `Kobako::PoolTimeoutError`. There is no teardown verb — a Pool releases everything with its own reachability.
|
|
453
|
-
|
|
454
|
-
#### Pool Trade-offs
|
|
455
|
-
|
|
456
|
-
| It gives you | It does not give you |
|
|
125
|
+
| Cap | Default |
|
|
457
126
|
|---|---|
|
|
458
|
-
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
### Service Blocks
|
|
463
|
-
|
|
464
|
-
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest. Its `break` / `next` / `return` / exceptions follow normal Ruby semantics: an exit lands where it would through a plain method.
|
|
465
|
-
|
|
466
|
-
```ruby
|
|
467
|
-
sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
|
|
468
|
-
|
|
469
|
-
sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }').value
|
|
470
|
-
# => [2, 4, 6]
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
A `yield` that cannot complete raises at the Service's call site, so the Service may rescue it. Left unrescued, a `BlockError` hands the guest its own exception back; the others fail the Service call.
|
|
127
|
+
| `timeout` | 60 s |
|
|
128
|
+
| `memory_limit` | 4 MiB |
|
|
129
|
+
| `stdout_limit`, `stderr_limit` | 1 MiB |
|
|
130
|
+
| `open_file_limit` | 1024 |
|
|
474
131
|
|
|
475
|
-
|
|
476
|
-
|----------------------------|---------------------------------------|
|
|
477
|
-
| `Kobako::BlockError` | The guest block raised |
|
|
478
|
-
| `Kobako::YieldValueError` | An argument cannot cross to the guest |
|
|
479
|
-
| `Kobako::UnsupportedError` | The Guest Binary serves no block |
|
|
480
|
-
| `ThreadError` | The block runs off the calling Fiber |
|
|
481
|
-
|
|
482
|
-
#### Block Fiber
|
|
483
|
-
|
|
484
|
-
A block runs only on the Fiber that called the Service. The guest's Wasm stack lives on that Fiber's native stack, so entering it from another raises `ThreadError` before the block runs. CRuby itself lets a block run anywhere; kobako narrows that to keep the guest's stack checks sound.
|
|
485
|
-
|
|
486
|
-
```
|
|
487
|
-
Service on the calling Fiber
|
|
488
|
-
├─ blk.call / yield ─> runs in the guest
|
|
489
|
-
├─ Thread.new { blk.call } ─> ThreadError
|
|
490
|
-
└─ Enumerator.new { |y| y << blk.call }
|
|
491
|
-
.next (runs its body on a Fiber) ─> ThreadError
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
`Enumerator#next` counts as another Fiber, because it runs the enumerator's body on one. Internal iteration such as `each` or `map` stays on the calling Fiber and runs the block normally.
|
|
495
|
-
|
|
496
|
-
### Handle Management
|
|
132
|
+
### Concurrency
|
|
497
133
|
|
|
498
|
-
A
|
|
134
|
+
A Sandbox keeps nothing between runs, so Threads may share one. `gvl:
|
|
135
|
+
:release` runs guest code in parallel, and `Kobako::Pool` keeps warm Sandboxes.
|
|
499
136
|
|
|
500
137
|
```ruby
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
def greet = "hi, #{@name}"
|
|
504
|
-
end
|
|
505
|
-
|
|
506
|
-
sandbox.bind("Factory::Make", ->(name) { Greeter.new(name) })
|
|
507
|
-
|
|
508
|
-
sandbox.eval('Factory::Make.call("Bob").greet').value # => "hi, Bob" (Handle round-trip inside guest)
|
|
509
|
-
sandbox.eval('Factory::Make.call("Bob")').value # => #<Greeter @name="Bob"> (Handle restoration)
|
|
138
|
+
pool = Kobako::Pool.new(slots: 4) { |sandbox| sandbox.bind("KV::Lookup", lookup) }
|
|
139
|
+
pool.with { |sandbox| sandbox.eval(script).value }
|
|
510
140
|
```
|
|
511
141
|
|
|
512
|
-
|
|
142
|
+
### Snippets
|
|
513
143
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
Each dispatch that hands back a non-wire-representable object allocates a *new* Handle — kobako never deduplicates by object identity. This is most visible with fluent / builder APIs. An `ActiveRecord::Relation` chain `spawn`s a fresh relation at each step, so every hop is an independent dispatch that binds its own Handle:
|
|
517
|
-
|
|
518
|
-
```
|
|
519
|
-
guest chain host (Catalog::Handles, one invocation)
|
|
520
|
-
─────────── ─────────────────────────────────────────
|
|
521
|
-
User.where(active: true) ─call──▶ Relation #1 (fresh clone) bound ▶ Handle 1
|
|
522
|
-
◀─Handle 1
|
|
523
|
-
.order(:created_at) ─call──▶ Relation #2 (fresh clone) bound ▶ Handle 2
|
|
524
|
-
◀─Handle 2
|
|
525
|
-
.limit(10) ─call──▶ Relation #3 (fresh clone) bound ▶ Handle 3
|
|
526
|
-
◀─Handle 3
|
|
527
|
-
|
|
528
|
-
3 hops ─▶ 3 dispatches ─▶ 3 distinct relations ─▶ 3 Handles
|
|
529
|
-
all stay live until the invocation ends, then reset together
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
This is deliberate, not a leak. Handle IDs run to 2³¹ − 1 per invocation and reset between invocations, so even deep chains stay far inside the range. Two consequences are worth keeping in mind. The same host object handed back twice yields two *different* Handles, and the guest cannot tell they alias. Every intermediate Handle stays live until the invocation ends, since there is no per-Handle release.
|
|
533
|
-
|
|
534
|
-
### Snippets & Entrypoints
|
|
535
|
-
|
|
536
|
-
`Sandbox#preload` registers named mruby snippets that replay into every invocation's canonical boot state; `Sandbox#run(:Target, *args, **kwargs)` dispatches into a top-level `Object` constant defined by those snippets.
|
|
144
|
+
`preload` replays guest code into every Invocation, and `run` calls what it
|
|
145
|
+
defined. An Extension pairs such code with a host backend.
|
|
537
146
|
|
|
538
147
|
```ruby
|
|
539
|
-
sandbox =
|
|
540
|
-
sandbox.
|
|
541
|
-
sandbox.preload(code: 'Greeter = ->(opts) { "hello, #{opts[:name]}" }', name: :Greeter)
|
|
542
|
-
|
|
543
|
-
sandbox.run(:Adder, 2, 3).value # => 5
|
|
544
|
-
sandbox.run(:Greeter, name: "world").value # => "hello, world"
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
An entrypoint's `kwargs` arrive as a trailing positional Hash, because mruby's C-side call path carries no keyword arguments. Declare a Hash parameter and unpack it yourself.
|
|
548
|
-
|
|
549
|
-
A target no snippet defined raises `Kobako::UndefinedEntrypointError`. Its `#available` lists the top-level constants the snippets did contribute, so the name is corrected from the error rather than by reading the guest source.
|
|
550
|
-
|
|
551
|
-
```
|
|
552
|
-
per-invocation replay (every #eval / #run, snippets in insertion order):
|
|
553
|
-
|
|
554
|
-
canonical boot state
|
|
555
|
-
│
|
|
556
|
-
├──▶ replay :Adder (defines Adder)
|
|
557
|
-
│
|
|
558
|
-
├──▶ replay :Greeter (defines Greeter)
|
|
559
|
-
│
|
|
560
|
-
└──▶ eval(source) -or- run(:Target, *args, **kwargs)
|
|
561
|
-
│
|
|
562
|
-
▼
|
|
563
|
-
return the Execution, then discard the instance
|
|
148
|
+
sandbox.preload(code: "Adder = ->(a, b) { a + b }", name: :Adder)
|
|
149
|
+
sandbox.run(:Adder, 2, 3).value # => 5
|
|
564
150
|
```
|
|
565
151
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
`#preload` accepts two payload forms:
|
|
569
|
-
|
|
570
|
-
| Form | Signature | Snippet name source | Validation timing |
|
|
571
|
-
|----------|--------------------------------------|---------------------------------------|----------------------------------------------------------------------------|
|
|
572
|
-
| Source | `preload(code: "...", name: :Const)` | The `name:` keyword | First invocation's replay; compile errors raise `Kobako::SandboxError` |
|
|
573
|
-
| Bytecode | `preload(binary: bytes)` | Read from the bytecode's `debug_info` | First invocation's replay; structural failure raises `Kobako::BytecodeError` |
|
|
574
|
-
|
|
575
|
-
Use the source form for snippets authored in your repo; use the bytecode form when snippets ship as build artifacts from a separate `mrbc` pipeline. Source is compiled once, before the first invocation, and replays as bytecode from then on, so the bytecode form saves only that one compile. Both replay through the same per-invocation path, so no snippet content failure surfaces at `#preload` — force the first replay with a no-op invocation (e.g. `sandbox.eval("nil")`) when you want validation before real traffic.
|
|
152
|
+
### Guest Binaries
|
|
576
153
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
An Extension teaches the guest a native-style constant by pairing a guest idiom (`source`) with an optional host `backend`. `Sandbox#install` composes the two through the existing `#preload` and `#bind` verbs, adding no wire or Guest Binary surface. Pure operations run in-guest with no round-trip. The rest dispatch to the backend under the same isolation and reflection guarantees as any bound Service ([`docs/guides/extensions.md`](docs/guides/extensions.md)).
|
|
154
|
+
The gem bundles the pure `kobako.wasm`. Each [GitHub Release] adds variants
|
|
155
|
+
with Regexp or JSON, and you can build your own in Rust.
|
|
580
156
|
|
|
581
157
|
```ruby
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
extend Kobako::Proxy
|
|
585
|
-
def self.basename(path) = path.split("/").last || "" # pure: runs in-guest
|
|
586
|
-
# read / write are undefined here, so they dispatch to the backend
|
|
587
|
-
end
|
|
588
|
-
MRUBY
|
|
589
|
-
|
|
590
|
-
sandbox.install(
|
|
591
|
-
Kobako::Extension.new(
|
|
592
|
-
name: :Vfs,
|
|
593
|
-
source: VFS,
|
|
594
|
-
backend: Kobako::Extension::Backend.new(
|
|
595
|
-
path: "Vfs",
|
|
596
|
-
provider: -> { CopyOnWriteFiles.new(root) } # invoked once per invocation
|
|
597
|
-
)
|
|
598
|
-
)
|
|
599
|
-
)
|
|
600
|
-
|
|
601
|
-
sandbox.eval('Vfs.read("sample.txt")') # dispatches to the backend's #read
|
|
158
|
+
sandbox = Kobako::Sandbox.new(wasm_path: "kobako+full.wasm")
|
|
159
|
+
sandbox.eval('JSON.generate({ n: 42 })').value # => "{\"n\":42}"
|
|
602
160
|
```
|
|
603
161
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
| Keyword | Lifetime |
|
|
607
|
-
|-------------|-----------------------------------------------------------------------------|
|
|
608
|
-
| `object:` | One object, shared by every invocation |
|
|
609
|
-
| `provider:` | A no-argument callable invoked per invocation |
|
|
610
|
-
| neither | A fillable, standing for `Kobako::Unresolved` until `ctx.bind` supplies the run's object |
|
|
611
|
-
|
|
612
|
-
A writable backend takes `provider:`, so its state cannot leak across runs. kobako ships no concrete Extension; the idiom and backend are yours. The [overlay VFS example](examples/vfs/) is a worked virtual file that reads through to disk while protecting it from guest writes.
|
|
162
|
+
[GitHub Release]: https://github.com/elct9620/kobako/releases
|
|
613
163
|
|
|
614
164
|
## Security
|
|
615
165
|
|
|
616
|
-
kobako isolates the guest, but
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
scoped to the task, not a capable one whose other methods leak more than you intend.
|
|
166
|
+
kobako isolates the guest, but it reaches whatever you `bind` or `mount`. Bind
|
|
167
|
+
an object built for the task, since every public method its class defines is
|
|
168
|
+
exposed.
|
|
620
169
|
|
|
621
170
|
```ruby
|
|
622
|
-
class ThemeReader
|
|
171
|
+
class ThemeReader # only #color is reachable
|
|
623
172
|
def color = AppConfig.theme.color
|
|
624
173
|
end
|
|
625
174
|
|
|
626
|
-
sandbox
|
|
627
|
-
sandbox.bind("Cfg::Settings", ThemeReader.new) # not: bind("Cfg::Settings", AppConfig)
|
|
628
|
-
|
|
629
|
-
sandbox.eval('Cfg::Settings.color').value # => "#3366ff" — any other method raises Kobako::NoServiceError
|
|
175
|
+
sandbox.bind("Cfg::Settings", ThemeReader.new) # not AppConfig itself
|
|
630
176
|
```
|
|
631
177
|
|
|
632
|
-
Guest code can name any `MyService::KV` path, but a forged name only resolves to
|
|
633
|
-
something you bound. What you bind is the real authorization gate. An object can also
|
|
634
|
-
narrow its own surface with a private `respond_to_guest?`. Give each trust context its own
|
|
635
|
-
Sandbox. [`docs/guides/security.md`](docs/guides/security.md) covers the rest: self-gating
|
|
636
|
-
objects, validating untrusted input, default-deny external effects, and the return surface.
|
|
637
|
-
|
|
638
178
|
## Performance
|
|
639
179
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
| Phase | Cost |
|
|
643
|
-
|--------------------------------------------------------------|-----------------------|
|
|
644
|
-
| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
|
|
645
|
-
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~1.4 ms one-time |
|
|
646
|
-
| Subsequent `Sandbox.new` (caches warm) | ~3 µs |
|
|
647
|
-
| Warm `#eval` with one Service call on a reused Sandbox | ~62 µs |
|
|
648
|
-
| Warm `#run(:Entrypoint, ...)` dispatch | ~57 µs |
|
|
649
|
-
| Service call amortized inside one invocation | ~4.9 µs |
|
|
650
|
-
| Snippet replay per invocation | ~2.7 µs each |
|
|
651
|
-
| Per additional idle Sandbox (RSS) | < 1 KB |
|
|
180
|
+
Figures are order-of-magnitude on macOS arm64 with Ruby 3.4.7; ratios hold
|
|
181
|
+
across machines. The [benchmark] suite gates every release on a +10% regression.
|
|
652
182
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
#### Throughput Levers
|
|
656
|
-
|
|
657
|
-
| Choice | What it changes |
|
|
183
|
+
| Phase | Cost |
|
|
658
184
|
|---|---|
|
|
659
|
-
|
|
|
660
|
-
|
|
|
661
|
-
| `
|
|
185
|
+
| first `Sandbox.new` per machine (JIT, then disk-cached) | ~500 ms |
|
|
186
|
+
| later `Sandbox.new` | ~3 µs |
|
|
187
|
+
| warm `#eval` with one Service call | ~62 µs |
|
|
662
188
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
```bash
|
|
666
|
-
bundle exec rake bench # every gated regression benchmark (~5-8 min)
|
|
667
|
-
```
|
|
189
|
+
[benchmark]: https://github.com/elct9620/kobako/tree/main/benchmark
|
|
668
190
|
|
|
669
191
|
## Development
|
|
670
192
|
|
|
671
|
-
|
|
193
|
+
The release gate compiles everything and runs every test and lint.
|
|
672
194
|
|
|
673
195
|
```bash
|
|
674
|
-
bin/setup
|
|
675
|
-
bundle exec rake
|
|
196
|
+
bin/setup # install dependencies
|
|
197
|
+
bundle exec rake # the release gate
|
|
198
|
+
bin/console # IRB with the gem loaded
|
|
199
|
+
bundle exec rake install # install the local checkout
|
|
676
200
|
```
|
|
677
201
|
|
|
678
|
-
|
|
202
|
+
The first compile builds mruby and the Guest Binary through the [beni] gem.
|
|
679
203
|
|
|
680
|
-
|
|
204
|
+
[beni]: https://github.com/elct9620/beni
|
|
681
205
|
|
|
682
|
-
|
|
206
|
+
## Contributing
|
|
683
207
|
|
|
684
|
-
|
|
208
|
+
Bug reports and pull requests are welcome at the [repository]. Open an issue
|
|
209
|
+
before a non-trivial change, so we can agree on its scope.
|
|
685
210
|
|
|
686
|
-
|
|
687
|
-
|---|---|---|
|
|
688
|
-
| Gem | the Ruby gem, with the bundled `kobako.wasm` | `v*` |
|
|
689
|
-
| Crates | the linked guest-crate group | `<component>-v*` |
|
|
211
|
+
[repository]: https://github.com/elct9620/kobako
|
|
690
212
|
|
|
691
213
|
## License
|
|
692
214
|
|