kobako 0.26.0-aarch64-linux → 0.28.0-aarch64-linux
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 +103 -0
- data/README.md +156 -118
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +38 -0
- data/crates/kobako-runtime/README.md +10 -12
- data/crates/kobako-transport/CHANGELOG.md +35 -0
- data/crates/kobako-transport/README.md +43 -27
- data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
- data/crates/kobako-wasmtime/README.md +8 -10
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +41 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/services.rb +7 -61
- data/lib/kobako/catalog/snippets.rb +42 -66
- data/lib/kobako/catalog.rb +2 -10
- data/lib/kobako/codec/decoder.rb +9 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +4 -10
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec.rb +3 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +51 -112
- data/lib/kobako/errors.rb +51 -127
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +23 -53
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +96 -133
- data/lib/kobako/sandbox_options.rb +38 -84
- data/lib/kobako/snippet/binary.rb +1 -16
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +1 -9
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +112 -195
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +5 -10
- data/lib/kobako/transport/reflection.rb +39 -50
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +21 -73
- data/lib/kobako/transport.rb +3 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +17 -0
- data/sig/kobako/catalog/services.rbs +0 -6
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec.rbs +2 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +2 -3
- data/sig/kobako/errors.rbs +6 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/runtime.rbs +12 -8
- data/sig/kobako/sandbox.rbs +4 -0
- data/sig/kobako/sandbox_options.rbs +2 -6
- data/sig/kobako/transport/dispatcher.rbs +14 -18
- data/sig/kobako/transport/reflection.rbs +5 -3
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +7 -6
- metadata +6 -3
- data/rust-toolchain.toml +0 -11
data/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://deepwiki.com/elct9620/kobako)
|
|
4
4
|
|
|
5
|
-
Kobako is a Ruby gem that embeds a Wasm-isolated mruby interpreter inside your application
|
|
5
|
+
Kobako is a Ruby gem that embeds a Wasm-isolated mruby interpreter inside your application. It runs untrusted Ruby scripts in-process — LLM-generated code, user formulas, student submissions, third-party plugins. Those scripts get no access to host memory, files, network, or credentials. Its home is the Ruby ecosystem; a Rust SDK offers the same sandbox to hosts written in other languages.
|
|
6
6
|
|
|
7
|
-
The host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby and a Transport proxy.
|
|
7
|
+
The host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby and a Transport proxy. A guest script reaches the outside world only through Host App-declared **Services**, the named host objects you explicitly inject into the sandbox. The guest sees each one as a proxy that forwards calls back to the host over the Transport wire.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
Host process Wasm guest
|
|
@@ -24,14 +24,19 @@ The host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby an
|
|
|
24
24
|
|
|
25
25
|
## Requirements
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
| What | Which versions | When you need it |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Ruby | ≥ 3.3.0 | always |
|
|
30
|
+
| Platform | Linux or macOS | always — Windows is not supported |
|
|
31
|
+
| Rust / Cargo | current stable | only where no prebuilt gem matches your platform; otherwise the native extension installs ready-built |
|
|
32
|
+
| WASI toolchain | vendored by the build | only when building the gem from a source checkout — see [Development](#development) |
|
|
30
33
|
|
|
31
|
-
The precompiled `kobako.wasm` Guest Binary ships inside the gem, so end users do **not** need a WASI toolchain.
|
|
34
|
+
The precompiled `kobako.wasm` Guest Binary ships inside the gem, so end users do **not** need a WASI toolchain.
|
|
32
35
|
|
|
33
36
|
## Installation
|
|
34
37
|
|
|
38
|
+
Install the gem; it ships its Guest Binary prebuilt.
|
|
39
|
+
|
|
35
40
|
```bash
|
|
36
41
|
bundle add kobako
|
|
37
42
|
# or
|
|
@@ -52,44 +57,43 @@ sandbox.eval("1 + 2").value # => 3
|
|
|
52
57
|
```rust
|
|
53
58
|
use kobako::{Options, Sandbox};
|
|
54
59
|
|
|
55
|
-
// Options::default() is secure by default:
|
|
60
|
+
// Options::default() is secure by default: the same caps as Ruby, hermetic isolation.
|
|
56
61
|
let sandbox = Sandbox::new("kobako.wasm", Options::default())?;
|
|
57
62
|
sandbox.eval("1 + 2")?.value()?; // => Value::Int(3)
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
Each invocation returns the record of that one run
|
|
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.
|
|
61
66
|
|
|
62
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.
|
|
63
68
|
|
|
64
69
|
## Frontends & Guest Binaries
|
|
65
70
|
|
|
66
|
-
Embedding kobako is two independent choices: the **host frontend** you build against, and the **Guest Binary** it runs.
|
|
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
|
+
```
|
|
67
79
|
|
|
68
|
-
How far down those choices go — and which ones a given starting point quietly makes for you — is laid out in [`docs/
|
|
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).
|
|
69
81
|
|
|
70
82
|
### Host frontends
|
|
71
83
|
|
|
72
|
-
The host embeds the sandbox and owns the
|
|
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.
|
|
73
85
|
|
|
74
86
|
| Frontend | Package | Add it | Best for |
|
|
75
87
|
|----------|---------|--------|----------|
|
|
76
88
|
| Ruby gem | `kobako` (RubyGems) | `gem install kobako` | A Ruby host — Services, Handles, snippets, and pooling out of the box |
|
|
77
|
-
| Rust SDK | `kobako` (crates.io) | `
|
|
78
|
-
| Low-level crates | `kobako-wasmtime` + `kobako-runtime` + `kobako-transport` + `kobako-codec` |
|
|
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 |
|
|
79
91
|
|
|
80
|
-
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
|
|
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.
|
|
81
93
|
|
|
82
94
|
### Pre-built Guest Binaries
|
|
83
95
|
|
|
84
|
-
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.
|
|
85
|
-
|
|
86
|
-
| Variant | File | Adds | Distribution |
|
|
87
|
-
|---------|------|------|--------------|
|
|
88
|
-
| Pure (default) | `kobako.wasm` | mruby + IO | Bundled in the gem, and a Release asset |
|
|
89
|
-
| +regexp | `kobako+regexp.wasm` | Regexp / MatchData (ASCII) | Release asset |
|
|
90
|
-
| +regexp-unicode | `kobako+regexp-unicode.wasm` | Regexp / MatchData (Unicode) | Release asset |
|
|
91
|
-
| +json | `kobako+json.wasm` | JSON | Release asset |
|
|
92
|
-
| +full | `kobako+full.wasm` | JSON + Regexp (ASCII) | Release asset |
|
|
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.
|
|
93
97
|
|
|
94
98
|
```ruby
|
|
95
99
|
sandbox = Kobako::Sandbox.new(wasm_path: "kobako+full.wasm")
|
|
@@ -98,60 +102,55 @@ sandbox.eval('JSON.generate({ n: "42".to_i })').value # => "{\"n\":42}"
|
|
|
98
102
|
|
|
99
103
|
### Custom Guest Binaries
|
|
100
104
|
|
|
101
|
-
When no
|
|
105
|
+
When no variant matches your capability set, build a Guest Binary of your own in Rust. A shell over `kobako-mruby` installs exactly the capability gems you choose: the shipped `kobako-io`, `kobako-regexp`, and `kobako-json`, or your own `beni::Gem`. `kobako-core` exports the wasm ABI. The Ruby gem drives it unchanged.
|
|
102
106
|
|
|
103
|
-
|
|
|
104
|
-
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
| `kobako-
|
|
108
|
-
| `kobako-regexp` | Regexp / MatchData capability gem |
|
|
109
|
-
| `kobako-json` | JSON capability gem |
|
|
107
|
+
| To decide | Read |
|
|
108
|
+
|---|---|
|
|
109
|
+
| which parts you own at each level | [`docs/guides/assembly-levels.md`](docs/guides/assembly-levels.md) |
|
|
110
|
+
| what a guest shell implements | [`docs/guides/customization.md`](docs/guides/customization.md) |
|
|
111
|
+
| a working shell | [`wasm/kobako-wasm/src/guest.rs`](wasm/kobako-wasm/src/guest.rs) |
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
struct MyGuest;
|
|
115
|
-
|
|
116
|
-
// Pick the capability gems the guest exposes.
|
|
117
|
-
impl kobako_mruby::MrbGuest for MyGuest {
|
|
118
|
-
fn init_gems(mrb: &Mrb) -> Result<(), Error> {
|
|
119
|
-
mrb.init_gem::<kobako_io::KobakoIo>()?;
|
|
120
|
-
mrb.init_gem::<kobako_json::KobakoJson>()?;
|
|
121
|
-
Ok(())
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Forward the ABI contract to the harness flows.
|
|
126
|
-
impl kobako_core::Guest for MyGuest {
|
|
127
|
-
fn eval() { <MyGuest as kobako_mruby::MrbGuest>::eval() }
|
|
128
|
-
fn run(env: &[u8]) { <MyGuest as kobako_mruby::MrbGuest>::run(env) }
|
|
129
|
-
fn yield_to_block(req: &[u8]) -> u64 { <MyGuest as kobako_mruby::MrbGuest>::yield_to_block(req) }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
kobako_core::export_guest!(MyGuest);
|
|
133
|
-
```
|
|
113
|
+
Build the shell as a `cdylib` for `wasm32-wasip1`, then bake its boot state into the artifact with `kobako-baker input.wasm output.wasm`.
|
|
114
|
+
|
|
115
|
+
A shell may also define flags, switches the Host App sets for every invocation. A flag is critical by default, so a Guest Binary that does not recognise it raises `Kobako::UnsupportedFlagError` rather than run without it. Mark it `advisory: true` only when a script cannot tell the flag was skipped.
|
|
134
116
|
|
|
135
|
-
|
|
117
|
+
```ruby
|
|
118
|
+
sandbox.flag(:strict_mode) # critical: refused where unrecognised
|
|
119
|
+
sandbox.flag(:compile_cache, "on", advisory: true) # skipped where unrecognised
|
|
120
|
+
```
|
|
136
121
|
|
|
137
122
|
## Glossary
|
|
138
123
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
|
142
|
-
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
148
|
-
|
|
|
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
|
+
| Context | `Kobako::Context` | The per-Invocation object the optional block receives. |
|
|
133
|
+
| Snippet | — | Guest source or bytecode replayed at the start of every Invocation. |
|
|
134
|
+
| Handle | `Kobako::Handle` | An opaque reference to a host object the wire cannot carry by value. |
|
|
135
|
+
| Block | — | Guest code passed alongside a Service call. |
|
|
136
|
+
|
|
137
|
+
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 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.
|
|
149
138
|
|
|
150
139
|
## Usage
|
|
151
140
|
|
|
141
|
+
Each section below stands on its own; this is the order they build in.
|
|
142
|
+
|
|
143
|
+
| If you want to | Read |
|
|
144
|
+
|---|---|
|
|
145
|
+
| let guest code call into your host | [Services](#services), [Per-Invocation Bindings](#per-invocation-bindings), [Service Blocks](#service-blocks) |
|
|
146
|
+
| see what a run produced or why it failed | [Output Capture](#output-capture), [Error Handling](#error-handling), [Invocation Lifecycle](#invocation-lifecycle) |
|
|
147
|
+
| bound what a run may consume | [Resource Limits](#resource-limits), [Concurrency](#concurrency), [Pooling](#pooling) |
|
|
148
|
+
| hand host objects across the boundary | [Handle Management](#handle-management) |
|
|
149
|
+
| ship guest code with the Sandbox | [Snippets & Entrypoints](#snippets--entrypoints), [Extensions](#extensions) |
|
|
150
|
+
|
|
152
151
|
### Services
|
|
153
152
|
|
|
154
|
-
`bind` any Ruby object as a Service at a constant-path name
|
|
153
|
+
`bind` any Ruby object as a Service at a constant-path name. The guest reaches it as a `MyService::KV` (or top-level `File`) proxy. Through the Transport wire, the guest invokes the public methods the object's own class defines.
|
|
155
154
|
|
|
156
155
|
```ruby
|
|
157
156
|
class User
|
|
@@ -171,11 +170,11 @@ sandbox.eval(<<~RUBY)
|
|
|
171
170
|
RUBY
|
|
172
171
|
```
|
|
173
172
|
|
|
174
|
-
Each `::`-separated path segment must match `/\A[A-Z]\w
|
|
173
|
+
Each `::`-separated path segment must match `/\A[A-Z]\w{0,65533}\z/`. 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`.
|
|
175
174
|
|
|
176
175
|
### Per-Invocation Bindings
|
|
177
176
|
|
|
178
|
-
A setup-time `bind` fixes one object for the Sandbox's life.
|
|
177
|
+
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.
|
|
179
178
|
|
|
180
179
|
```ruby
|
|
181
180
|
sandbox.bind("Req::Current") # declared, unfilled — stands for Kobako::Unresolved
|
|
@@ -183,11 +182,11 @@ sandbox.bind("Req::Current") # declared, unfilled — stands for Kobako::Unreso
|
|
|
183
182
|
sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
|
|
184
183
|
```
|
|
185
184
|
|
|
186
|
-
`ctx.bind` also shadows an already-bound path for that one run. The Context is spent once the block returns, so a `ctx` captured out of it raises `ArgumentError`. Choosing the object per invocation
|
|
185
|
+
`ctx.bind` also shadows an already-bound path for that one run. The Context is spent once the block returns, so a `ctx` captured out of it raises `ArgumentError`. Choosing the object per invocation lets concurrent Threads invoke a single Sandbox and still keep their identities apart. The [multi-tenant example](examples/multi-tenant/) runs that shape end to end.
|
|
187
186
|
|
|
188
187
|
### Output Capture
|
|
189
188
|
|
|
190
|
-
Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and read off the run's Execution, independently of its `#value
|
|
189
|
+
Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and read off the run's Execution, independently of its `#value`. Each invocation captures its own; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
|
|
191
190
|
|
|
192
191
|
```ruby
|
|
193
192
|
execution = sandbox.eval(<<~RUBY)
|
|
@@ -209,30 +208,34 @@ Every invocation either returns an Execution or raises exactly one of three clas
|
|
|
209
208
|
begin
|
|
210
209
|
sandbox.eval(script)
|
|
211
210
|
rescue Kobako::TrapError
|
|
212
|
-
# Wasm engine fault or cap exhaustion.
|
|
211
|
+
# Wasm engine fault or cap exhaustion. The invocation is over.
|
|
213
212
|
rescue Kobako::ServiceError
|
|
214
213
|
# A host Service call failed and the script did not rescue it.
|
|
215
214
|
rescue Kobako::SandboxError => e
|
|
216
|
-
# The script raised, failed to compile, or returned an unrepresentable value
|
|
215
|
+
# The script raised, failed to compile, or returned an unrepresentable value,
|
|
216
|
+
# or the Guest Binary could not start the run.
|
|
217
217
|
logger.warn(e.execution.stderr) # the failed run's own captures and usage
|
|
218
218
|
end
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
Each of these carries the failed run's Execution on `#execution
|
|
221
|
+
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.
|
|
222
222
|
|
|
223
223
|
| Class | Parent | Trigger |
|
|
224
224
|
|---------------------------------|----------------|------------------------------------------------------|
|
|
225
225
|
| `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
|
|
226
226
|
| `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
|
|
227
|
+
| `Kobako::NoServiceError` | `ServiceError` | Nothing bound there, a dead Handle, or a method the guest may not call |
|
|
228
|
+
| `Kobako::ServiceArgumentError` | `ServiceError` | The arguments did not fit the Service method |
|
|
227
229
|
| `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
|
|
228
230
|
| `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
|
|
231
|
+
| `Kobako::UnsupportedFlagError` | `SandboxError` | The Guest Binary does not recognise a critical `#flag`; no script ran |
|
|
229
232
|
| `Kobako::UndefinedEntrypointError` | `SandboxError` | `#run` named a constant no snippet defined; carries `#name` and `#available` |
|
|
230
233
|
|
|
231
234
|
`SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` fields when the guest produced a panic envelope.
|
|
232
235
|
|
|
233
236
|
### Resource Limits
|
|
234
237
|
|
|
235
|
-
Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Execution#usage`](lib/kobako/execution.rb) for actual consumption — populated on every outcome, so a rescued trap reports it just as a completed run does
|
|
238
|
+
Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Execution#usage`](lib/kobako/execution.rb) for actual consumption — populated on every outcome, so a rescued trap reports it just as a completed run does.
|
|
236
239
|
|
|
237
240
|
```ruby
|
|
238
241
|
sandbox = Kobako::Sandbox.new(
|
|
@@ -252,19 +255,26 @@ sandbox = Kobako::Sandbox.new(
|
|
|
252
255
|
|
|
253
256
|
`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.
|
|
254
257
|
|
|
255
|
-
Beyond the four caps, `profile:` requests the Sandbox's isolation posture on the `:permissive` < `:hermetic` ladder (default `:hermetic`). `:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources
|
|
258
|
+
Beyond the four caps, `profile:` requests the Sandbox's isolation posture on the `:permissive` < `:hermetic` ladder (default `:hermetic`). `: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. Filesystem, environment, and network stay unreachable 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.
|
|
256
259
|
|
|
257
260
|
### Concurrency
|
|
258
261
|
|
|
259
|
-
A Sandbox keeps no state from any run, so concurrent Threads may invoke distinct Sandboxes or share a single one
|
|
262
|
+
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.
|
|
263
|
+
|
|
264
|
+
| What you bind | Who reaches it | What it owes |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| Bound once at setup | every Thread sharing the Sandbox | it must itself be thread-safe |
|
|
267
|
+
| Supplied per invocation — `ctx.bind`, or an Extension `provider:` | that invocation alone | nothing |
|
|
260
268
|
|
|
261
|
-
|
|
269
|
+
#### GVL Modes
|
|
270
|
+
|
|
271
|
+
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.
|
|
262
272
|
|
|
263
273
|
```ruby
|
|
264
274
|
sandbox = Kobako::Sandbox.new(gvl: :release)
|
|
265
275
|
```
|
|
266
276
|
|
|
267
|
-
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
|
|
277
|
+
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.
|
|
268
278
|
|
|
269
279
|
### Invocation Lifecycle
|
|
270
280
|
|
|
@@ -315,13 +325,11 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
315
325
|
Services + snippets persist; invocation N+1 repeats.
|
|
316
326
|
```
|
|
317
327
|
|
|
318
|
-
For workloads that must be isolated from each other
|
|
328
|
+
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.
|
|
319
329
|
|
|
320
330
|
### Pooling
|
|
321
331
|
|
|
322
|
-
For hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes
|
|
323
|
-
|
|
324
|
-
`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. The build bakes the shared boot state into the artifact ([`mruby.md`](docs/spec/behavior/mruby.md)) and every dynamic script still compiles and runs per invocation, so all a pool actually saves is the host-side `Sandbox.new` — now under 3 µs, an order of magnitude below the invocation that follows it. For the workload kobako is built for — many small, short-lived Sandboxes running dynamic scripts — that is not a gain worth the coupling. What a Pool buys is warm setup and exclusive checkout, not isolation: a Sandbox holds no state from any run, so Threads sharing one are equally safe (see [Concurrency](#concurrency)).
|
|
332
|
+
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.
|
|
325
333
|
|
|
326
334
|
```ruby
|
|
327
335
|
pool = Kobako::Pool.new(slots: 4) do |sandbox|
|
|
@@ -331,16 +339,26 @@ end
|
|
|
331
339
|
pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))).value }
|
|
332
340
|
```
|
|
333
341
|
|
|
342
|
+
#### Options and lifetime
|
|
343
|
+
|
|
334
344
|
| Option | Meaning | Default |
|
|
335
345
|
|--------|---------|---------|
|
|
336
346
|
| `slots:` | Upper bound on constructed Sandboxes | required |
|
|
337
347
|
| `checkout_timeout:` | Seconds `#with` waits for a free Sandbox; `nil` waits indefinitely | 5.0 |
|
|
338
348
|
|
|
339
|
-
Sandboxes construct lazily on first demand. `#with` yields a Sandbox and returns the block's value
|
|
349
|
+
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.
|
|
350
|
+
|
|
351
|
+
#### Pool Trade-offs
|
|
352
|
+
|
|
353
|
+
| It gives you | It does not give you |
|
|
354
|
+
|---|---|
|
|
355
|
+
| warm, pre-configured Sandboxes and exclusive checkout | isolation (see [Concurrency](#concurrency)) |
|
|
356
|
+
|
|
357
|
+
A Sandbox holds no state from any run, so Threads sharing one are equally safe. `Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. The build bakes the shared boot state into the artifact, and every dynamic script still compiles and runs per invocation. All a pool actually saves is the host-side `Sandbox.new` — now about 3 µs, an order of magnitude below the invocation that follows it. For the workload kobako is built for — many small, short-lived Sandboxes running dynamic scripts — that is not a gain worth the coupling.
|
|
340
358
|
|
|
341
359
|
### Service Blocks
|
|
342
360
|
|
|
343
|
-
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest; `break` / `next` / exceptions follow normal Ruby semantics, scoped to the single dispatch.
|
|
361
|
+
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest; `break` / `next` / exceptions follow normal Ruby semantics, scoped to the single dispatch.
|
|
344
362
|
|
|
345
363
|
```ruby
|
|
346
364
|
sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
|
|
@@ -349,9 +367,17 @@ sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }').value
|
|
|
349
367
|
# => [2, 4, 6]
|
|
350
368
|
```
|
|
351
369
|
|
|
370
|
+
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 other two fail the Service call.
|
|
371
|
+
|
|
372
|
+
| Class | Trigger at `yield` |
|
|
373
|
+
|----------------------------|---------------------------------------|
|
|
374
|
+
| `Kobako::BlockError` | The guest block raised |
|
|
375
|
+
| `Kobako::YieldValueError` | An argument cannot cross to the guest |
|
|
376
|
+
| `Kobako::UnsupportedError` | The Guest Binary serves no block |
|
|
377
|
+
|
|
352
378
|
### Handle Management
|
|
353
379
|
|
|
354
|
-
A
|
|
380
|
+
A host object the wire cannot represent may be returned from a Service, passed to `#run`, or handed back from the guest. It crosses the boundary as an opaque `Kobako::Handle` proxy and is restored to the original object before host code sees it. Any other unrepresentable value raises `Kobako::SandboxError`. Handles are scoped to a single invocation.
|
|
355
381
|
|
|
356
382
|
```ruby
|
|
357
383
|
class Greeter
|
|
@@ -365,9 +391,11 @@ sandbox.eval('Factory::Make.call("Bob").greet').value # => "hi, Bob" (Handle r
|
|
|
365
391
|
sandbox.eval('Factory::Make.call("Bob")').value # => #<Greeter @name="Bob"> (Handle restoration)
|
|
366
392
|
```
|
|
367
393
|
|
|
368
|
-
A `break` value from a guest block is the one exception
|
|
394
|
+
A `break` value from a guest block is the one exception. It unwinds back to the guest Service call rather than to host code, so a Handle in it stays a Handle. Restoring would just re-wrap the same object into a new id on the return trip.
|
|
395
|
+
|
|
396
|
+
#### Per-dispatch Handles
|
|
369
397
|
|
|
370
|
-
Each dispatch that hands back a non-wire-representable object allocates a *new* Handle — kobako never deduplicates by object identity
|
|
398
|
+
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:
|
|
371
399
|
|
|
372
400
|
```
|
|
373
401
|
guest chain host (Catalog::Handles, one invocation)
|
|
@@ -383,11 +411,11 @@ Each dispatch that hands back a non-wire-representable object allocates a *new*
|
|
|
383
411
|
all stay live until the invocation ends, then reset together
|
|
384
412
|
```
|
|
385
413
|
|
|
386
|
-
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
|
|
414
|
+
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.
|
|
387
415
|
|
|
388
416
|
### Snippets & Entrypoints
|
|
389
417
|
|
|
390
|
-
`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
|
|
418
|
+
`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.
|
|
391
419
|
|
|
392
420
|
```ruby
|
|
393
421
|
sandbox = Kobako::Sandbox.new
|
|
@@ -398,9 +426,9 @@ sandbox.run(:Adder, 2, 3).value # => 5
|
|
|
398
426
|
sandbox.run(:Greeter, name: "world").value # => "hello, world"
|
|
399
427
|
```
|
|
400
428
|
|
|
401
|
-
An entrypoint's `kwargs` arrive as a trailing positional Hash
|
|
429
|
+
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.
|
|
402
430
|
|
|
403
|
-
A target no snippet defined raises `Kobako::UndefinedEntrypointError
|
|
431
|
+
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.
|
|
404
432
|
|
|
405
433
|
```
|
|
406
434
|
per-invocation replay (every #eval / #run, snippets in insertion order):
|
|
@@ -417,6 +445,8 @@ A target no snippet defined raises `Kobako::UndefinedEntrypointError`, whose `#a
|
|
|
417
445
|
return the Execution, then discard the instance
|
|
418
446
|
```
|
|
419
447
|
|
|
448
|
+
#### Payload Forms
|
|
449
|
+
|
|
420
450
|
`#preload` accepts two payload forms:
|
|
421
451
|
|
|
422
452
|
| Form | Signature | Snippet name source | Validation timing |
|
|
@@ -424,11 +454,11 @@ A target no snippet defined raises `Kobako::UndefinedEntrypointError`, whose `#a
|
|
|
424
454
|
| Source | `preload(code: "...", name: :Const)` | The `name:` keyword | First invocation's replay; compile errors raise `Kobako::SandboxError` |
|
|
425
455
|
| Bytecode | `preload(binary: bytes)` | Read from the bytecode's `debug_info` | First invocation's replay; structural failure raises `Kobako::BytecodeError` |
|
|
426
456
|
|
|
427
|
-
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. 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.
|
|
457
|
+
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.
|
|
428
458
|
|
|
429
459
|
### Extensions
|
|
430
460
|
|
|
431
|
-
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
|
|
461
|
+
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)).
|
|
432
462
|
|
|
433
463
|
```ruby
|
|
434
464
|
FILE = <<~'MRUBY'
|
|
@@ -458,17 +488,17 @@ A backend declares the bound object's lifetime by keyword, never by inference
|
|
|
458
488
|
| Keyword | Lifetime |
|
|
459
489
|
|-------------|-----------------------------------------------------------------------------|
|
|
460
490
|
| `object:` | One object, shared by every invocation |
|
|
461
|
-
| `provider:` | A no-argument callable invoked per invocation
|
|
491
|
+
| `provider:` | A no-argument callable invoked per invocation |
|
|
462
492
|
| neither | A fillable, standing for `Kobako::Unresolved` until `ctx.bind` supplies the run's object |
|
|
463
493
|
|
|
464
|
-
kobako ships no concrete Extension; the idiom and backend are yours. The [overlay VFS example](examples/vfs/) is a worked `File` that reads through to disk while protecting it from guest writes.
|
|
494
|
+
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 `File` that reads through to disk while protecting it from guest writes.
|
|
465
495
|
|
|
466
496
|
## Security
|
|
467
497
|
|
|
468
498
|
kobako isolates the guest, but **what it may reach is whatever you `bind`** — and `bind`
|
|
469
499
|
exposes every public method the object's own class defines. Inherited, mixed-in, and built-in
|
|
470
|
-
methods stay out of reach, but the class's own surface does not
|
|
471
|
-
|
|
500
|
+
methods stay out of reach, but the class's own surface does not. Bind a purpose-built object
|
|
501
|
+
scoped to the task, not a capable one whose other methods leak more than you intend.
|
|
472
502
|
|
|
473
503
|
```ruby
|
|
474
504
|
class ThemeReader # only #color is reachable; AppConfig.secret_key is not
|
|
@@ -478,19 +508,14 @@ end
|
|
|
478
508
|
sandbox = Kobako::Sandbox.new
|
|
479
509
|
sandbox.bind("Cfg::Settings", ThemeReader.new) # not: bind("Cfg::Settings", AppConfig)
|
|
480
510
|
|
|
481
|
-
sandbox.eval('Cfg::Settings.color').value # => "#3366ff" —
|
|
511
|
+
sandbox.eval('Cfg::Settings.color').value # => "#3366ff" — any other method raises Kobako::NoServiceError
|
|
482
512
|
```
|
|
483
513
|
|
|
484
|
-
When a purpose-built wrapper is more than you need, an object can gate its own surface in
|
|
485
|
-
place: a private `respond_to_guest?(name)` answers, per method, whether the guest may call
|
|
486
|
-
it. Returning `false` for every name makes the object opaque — a credential the guest
|
|
487
|
-
forwards to another Service but never reads — while permitting a named subset exposes exactly those.
|
|
488
|
-
|
|
489
514
|
Guest code can name any `MyService::KV` path, but a forged name only resolves to
|
|
490
|
-
something you bound
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
and
|
|
515
|
+
something you bound. What you bind is the real authorization gate. An object can also
|
|
516
|
+
narrow its own surface with a private `respond_to_guest?`. Give each trust context its own
|
|
517
|
+
Sandbox. [`docs/guides/security.md`](docs/guides/security.md) covers the rest: self-gating
|
|
518
|
+
objects, validating untrusted input, default-deny external effects, and the return surface.
|
|
494
519
|
|
|
495
520
|
## Performance
|
|
496
521
|
|
|
@@ -499,17 +524,25 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
|
|
|
499
524
|
| Phase | Cost |
|
|
500
525
|
|--------------------------------------------------------------|-----------------------|
|
|
501
526
|
| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
|
|
502
|
-
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~
|
|
527
|
+
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~3 ms one-time |
|
|
503
528
|
| Subsequent `Sandbox.new` (caches warm) | ~3 µs |
|
|
504
|
-
| Warm `#eval` with one Service call on a reused Sandbox | ~
|
|
505
|
-
| Warm `#run(:Entrypoint, ...)` dispatch | ~
|
|
506
|
-
| Service call amortized inside one invocation | ~5.
|
|
507
|
-
| Snippet replay per invocation | ~7.
|
|
508
|
-
| Per additional idle Sandbox (RSS) |
|
|
529
|
+
| Warm `#eval` with one Service call on a reused Sandbox | ~70 µs |
|
|
530
|
+
| Warm `#run(:Entrypoint, ...)` dispatch | ~79 µs |
|
|
531
|
+
| Service call amortized inside one invocation | ~5.8 µs |
|
|
532
|
+
| Snippet replay per invocation | ~7.0 µs each |
|
|
533
|
+
| Per additional idle Sandbox (RSS) | < 1 KB |
|
|
509
534
|
|
|
510
|
-
The Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance
|
|
535
|
+
The Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance, which is why a thousand idle tenants cost ~34 MB total. The canonical boot state is baked into the artifact and instantiated per invocation. A +10% regression on any gated benchmark blocks release.
|
|
511
536
|
|
|
512
|
-
|
|
537
|
+
#### Throughput Levers
|
|
538
|
+
|
|
539
|
+
| Choice | What it changes |
|
|
540
|
+
|---|---|
|
|
541
|
+
| `gvl: :hold` (default) | ~16k `#eval`/s in aggregate, at any Thread count |
|
|
542
|
+
| `gvl: :release` | lifts that ceiling for compute-bound scripts (see [Concurrency](#concurrency)) |
|
|
543
|
+
| `match?` over `=~` | `=~` (~5 µs/match) costs about 5× `match?` (~1.0 µs) |
|
|
544
|
+
|
|
545
|
+
Under `:hold` the GVL serializes wasm work, though Ruby-side `#eval` setup still overlaps. `=~` eagerly builds the `MatchData` and match globals, so prefer `match?` for boolean tests. Regexp is an opt-in capability gem, excluded from the default binary and the gated set. Its throughput is tracked in a separate non-gated characterization (`#11` in [`benchmark/README.md`](benchmark/README.md)).
|
|
513
546
|
|
|
514
547
|
```bash
|
|
515
548
|
bundle exec rake bench # every gated regression benchmark (~5-8 min)
|
|
@@ -521,16 +554,21 @@ After checking out the repo:
|
|
|
521
554
|
|
|
522
555
|
```bash
|
|
523
556
|
bin/setup # install dependencies
|
|
524
|
-
bundle exec rake #
|
|
557
|
+
bundle exec rake # the release gate: compile, tests, rubocop, steep, gate checks
|
|
525
558
|
```
|
|
526
559
|
|
|
527
|
-
Building from source requires a WASI-capable Rust toolchain in addition to the standard host toolchain
|
|
560
|
+
Building from source requires a WASI-capable Rust toolchain in addition to the standard host toolchain. The first compile walks the full chain. The [beni](https://github.com/elct9620/beni) gem vendors wasi-sdk + mruby and builds `libmruby.a` (`rake beni:build`), then `rake wasm:build` produces the Guest Binary. See [`AGENTS.md`](AGENTS.md) for the rake task map and pipeline layout. `bin/console` opens an IRB session with the gem preloaded; `bundle exec rake install` installs the local checkout as a gem.
|
|
528
561
|
|
|
529
562
|
## Contributing
|
|
530
563
|
|
|
531
564
|
Bug reports and pull requests are welcome at <https://github.com/elct9620/kobako>. Please open an issue before starting on non-trivial changes so we can align on scope.
|
|
532
565
|
|
|
533
|
-
Releases are automated with release-please across two tracks
|
|
566
|
+
Releases are automated with release-please across two tracks; the version rules, commit conventions, and how to cut each release live in [`docs/releasing.md`](docs/releasing.md).
|
|
567
|
+
|
|
568
|
+
| Track | What it ships | Tag |
|
|
569
|
+
|---|---|---|
|
|
570
|
+
| Gem | the Ruby gem, with the bundled `kobako.wasm` | `v*` |
|
|
571
|
+
| Crates | the linked guest-crate group | `<component>-v*` |
|
|
534
572
|
|
|
535
573
|
## License
|
|
536
574
|
|
data/ROADMAP.md
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
kobako
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`#run` dispatch, Service injection at constant-path names, opaque
|
|
7
|
-
Capability Handles, block yield re-entry, three-class error attribution,
|
|
8
|
-
output capture, and a warm Sandbox pool.
|
|
3
|
+
kobako runs untrusted mruby code in an in-process Wasm sandbox. A wasmtime
|
|
4
|
+
host drives a precompiled `kobako.wasm` guest over a MessagePack wire. Each
|
|
5
|
+
row below links a feature to its scenarios and to where its code starts.
|
|
9
6
|
|
|
10
|
-
| Feature | Entry Points |
|
|
11
|
-
|
|
12
|
-
| ✅ [
|
|
13
|
-
| ✅ [
|
|
14
|
-
| ✅ [
|
|
15
|
-
| ✅ [
|
|
16
|
-
| ✅ [
|
|
17
|
-
| ✅ [
|
|
18
|
-
| ✅ [
|
|
19
|
-
| ✅ [
|
|
20
|
-
| ✅ [
|
|
21
|
-
| ✅ [
|
|
22
|
-
| ✅ [
|
|
23
|
-
| ✅ [
|
|
24
|
-
| ✅ [
|
|
25
|
-
| ✅ [
|
|
7
|
+
| Feature | Entry Points |
|
|
8
|
+
|---------|-------------|
|
|
9
|
+
| ✅ [Sandbox](docs/spec/behavior/sandbox.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) |
|
|
10
|
+
| ✅ [Service registration](docs/spec/behavior/services.md) | [lib/kobako/catalog/services.rb](lib/kobako/catalog/services.rb) |
|
|
11
|
+
| ✅ [Runtime](docs/spec/behavior/runtime.md) | [lib/kobako/runtime.rb](lib/kobako/runtime.rb) |
|
|
12
|
+
| ✅ [Pool](docs/spec/behavior/pool.md) | [lib/kobako/pool.rb](lib/kobako/pool.rb) |
|
|
13
|
+
| ✅ [Extension](docs/spec/behavior/extension.md) | [lib/kobako/extension.rb](lib/kobako/extension.rb) |
|
|
14
|
+
| ✅ [Outcome attribution](docs/spec/behavior/outcome.md) | [lib/kobako/outcome.rb](lib/kobako/outcome.rb) |
|
|
15
|
+
| ✅ [Transport dispatch](docs/spec/behavior/transport-dispatch.md) | [lib/kobako/transport/dispatcher.rb](lib/kobako/transport/dispatcher.rb) |
|
|
16
|
+
| ✅ [Yield re-entry](docs/spec/behavior/transport-yield.md) | [lib/kobako/transport/yielder.rb](lib/kobako/transport/yielder.rb) |
|
|
17
|
+
| ✅ [Dispatch boundary](docs/spec/behavior/transport-boundary.md) | [lib/kobako/transport/exposure.rb](lib/kobako/transport/exposure.rb) |
|
|
18
|
+
| ✅ [Payload encoding](docs/spec/behavior/payload-encoding.md) | [lib/kobako/payload/](lib/kobako/payload/) |
|
|
19
|
+
| ✅ [Payload wire](docs/spec/behavior/codec.md) | [lib/kobako/codec/](lib/kobako/codec/) |
|
|
20
|
+
| ✅ [Core envelope](docs/spec/behavior/envelope.md) | [crates/kobako-transport/](crates/kobako-transport/) |
|
|
21
|
+
| ✅ [mruby guest](docs/spec/behavior/mruby.md) | [wasm/kobako-mruby/](wasm/kobako-mruby/) |
|
|
22
|
+
| ✅ [Guest IO](docs/spec/behavior/io.md) | [wasm/kobako-io/](wasm/kobako-io/) |
|
|
23
|
+
| ✅ [Guest JSON](docs/spec/behavior/json.md) | [wasm/kobako-json/](wasm/kobako-json/) |
|
|
24
|
+
| ✅ [Regexp](docs/spec/behavior/regexp.md) | [wasm/kobako-regexp/src/regexp.rs](wasm/kobako-regexp/src/regexp.rs) |
|
|
25
|
+
| ✅ [Regexp over String](docs/spec/behavior/regexp-string.md) | [wasm/kobako-regexp/src/string_ext.rs](wasm/kobako-regexp/src/string_ext.rs) |
|
|
26
|
+
| ✅ [MatchData](docs/spec/behavior/regexp-matchdata.md) | [wasm/kobako-regexp/src/matchdata.rs](wasm/kobako-regexp/src/matchdata.rs) |
|