kobako 0.14.0 → 0.16.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 +67 -0
- data/Cargo.lock +3 -3
- data/README.md +104 -29
- data/ROADMAP.md +2 -3
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/snapshot.rs +7 -3
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/capture.rs +17 -13
- data/crates/kobako-wasmtime/src/driver.rs +6 -7
- data/crates/kobako-wasmtime/src/frames.rs +2 -9
- data/crates/kobako-wasmtime/src/guest_mem.rs +8 -1
- data/crates/kobako-wasmtime/src/trap.rs +30 -57
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/runtime/errors.rs +9 -39
- data/ext/kobako/src/runtime.rs +5 -20
- data/lib/kobako/catalog/extensions.rb +114 -0
- data/lib/kobako/catalog/handles.rb +1 -1
- data/lib/kobako/catalog/services.rb +135 -0
- data/lib/kobako/catalog/snippets.rb +1 -1
- data/lib/kobako/catalog.rb +7 -5
- data/lib/kobako/codec/decoder.rb +3 -3
- data/lib/kobako/codec/encoder.rb +4 -4
- data/lib/kobako/codec/ext_types.rb +169 -0
- data/lib/kobako/codec/state.rb +98 -0
- data/lib/kobako/codec/utils.rb +2 -2
- data/lib/kobako/codec.rb +23 -6
- data/lib/kobako/extension.rb +47 -0
- data/lib/kobako/fault.rb +1 -1
- data/lib/kobako/handle.rb +1 -1
- data/lib/kobako/outcome.rb +13 -7
- data/lib/kobako/pool.rb +1 -1
- data/lib/kobako/sandbox.rb +47 -27
- data/lib/kobako/transport/dispatcher.rb +25 -18
- data/lib/kobako/transport/request.rb +14 -9
- data/lib/kobako/transport/response.rb +9 -2
- data/lib/kobako/transport/yield.rb +4 -1
- data/lib/kobako/transport/yielder.rb +14 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +25 -0
- data/sig/kobako/catalog/services.rbs +27 -0
- data/sig/kobako/codec/ext_types.rbs +31 -0
- data/sig/kobako/codec/state.rbs +20 -0
- data/sig/kobako/codec.rbs +3 -0
- data/sig/kobako/extension.rbs +20 -0
- data/sig/kobako/sandbox.rbs +3 -1
- data/sig/kobako/transport/dispatcher.rbs +4 -4
- data/sig/kobako/transport/yielder.rbs +1 -1
- data/sig/kobako/transport.rbs +2 -2
- metadata +11 -7
- data/lib/kobako/catalog/namespaces.rb +0 -115
- data/lib/kobako/codec/factory.rb +0 -187
- data/lib/kobako/namespace.rb +0 -78
- data/sig/kobako/catalog/namespaces.rbs +0 -17
- data/sig/kobako/codec/factory.rbs +0 -34
- data/sig/kobako/namespace.rbs +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8298cb68a22a8100ea0ab330902a1d772049e990a35aa154b9233429f5c43b22
|
|
4
|
+
data.tar.gz: 9a1ae245d152dab877fc3104c157eab78a38fc15cbff3e94baf187eb955d4e35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1aabea83fa046f0e5f9cc6ae3600e0f69cd045d38d3ecdcd91c6e71cbc19d29946ecd200ca44cc7941af329300d0f20a5733585a341e810ef19522fb083b4513
|
|
7
|
+
data.tar.gz: 23e5eccfa72eca1b6c39144c7d552a0eaf1a8467342fae504799744e8b4fee4036ad1d27a3e2311f0929cb300b74e04183ec9c8e3499043257860e9779c181ce
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.16.0","wasm/kobako-core":"0.10.0","wasm/kobako-mruby":"0.10.0","wasm/kobako-io":"0.10.0","wasm/kobako-json":"0.10.0","wasm/kobako-regexp":"0.10.0","wasm/kobako-baker":"0.10.0","crates/kobako-codec":"0.10.0","crates/kobako-runtime":"0.10.0","crates/kobako-wasmtime":"0.10.0","crates/kobako":"0.10.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.16.0](https://github.com/elct9620/kobako/compare/v0.15.0...v0.16.0) (2026-07-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **sandbox:** add the Extension install mechanism ([ad8e4da](https://github.com/elct9620/kobako/commit/ad8e4da703e4ceee85959663d77bb1e26c26791a))
|
|
9
|
+
* **sdk:** add the Extension install mechanism to the Rust host SDK ([4043f76](https://github.com/elct9620/kobako/commit/4043f764b7038619a30c16542ed38d566e4a72a9))
|
|
10
|
+
* **tasks:** break each module down by language in stats:<slug> ([b496e3c](https://github.com/elct9620/kobako/commit/b496e3cb23150f8f073cc1b972978cfbebfbb1e6))
|
|
11
|
+
* **tasks:** report code sizes per publishable module in stats:all ([f554930](https://github.com/elct9620/kobako/commit/f554930e669a82bed0c7305c5cbbab96c3ae79da))
|
|
12
|
+
* **tasks:** split impl vs inline test per module in stats:all ([cefa38d](https://github.com/elct9620/kobako/commit/cefa38d96ff79509b947269ea5c361ead0610bfa))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **tasks:** count Rust inline tests in the stats code-to-test ratio ([134c970](https://github.com/elct9620/kobako/commit/134c9704a5a9bb6fb01c1aa7968f907d1436e5f2))
|
|
18
|
+
|
|
19
|
+
## [0.15.0](https://github.com/elct9620/kobako/compare/v0.14.0...v0.15.0) (2026-07-11)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **claude:** cover rake files in the post-edit rubocop hook ([416cd42](https://github.com/elct9620/kobako/commit/416cd42009e7888fb0662e91719f4d7d61713cf8))
|
|
25
|
+
* **examples:** add plugin-rs, a Rust SDK narrative example ([8263096](https://github.com/elct9620/kobako/commit/8263096876900aa2c3ffeebbd24f7cca3571b893))
|
|
26
|
+
* **examples:** add wire-rs, a low-level host without the SDK ([d9edea8](https://github.com/elct9620/kobako/commit/d9edea8ed025e4b7ffe58be8c376c7fb0a65ac04))
|
|
27
|
+
* **sandbox:** flatten Service registration to path-valued bind ([0876006](https://github.com/elct9620/kobako/commit/0876006455544fd82eb7555ee80c149d98843719))
|
|
28
|
+
* **tasks:** add grouped test:tasks / test:bench runs ([1153c43](https://github.com/elct9620/kobako/commit/1153c43416363d7cc03b696eb2762b7281a68e93))
|
|
29
|
+
* **tasks:** compare the wire-symmetric peer inventories mechanically ([50d4fe5](https://github.com/elct9620/kobako/commit/50d4fe523bf85406931fa9611b6528c3de1a3f50))
|
|
30
|
+
* **tasks:** flag stale ledger entries in the surface and symmetry instruments ([d18054f](https://github.com/elct9620/kobako/commit/d18054f5daa6ce277154c288d5c3085133f441b4))
|
|
31
|
+
* **tasks:** hold the pub-surface crate map to the repo's crate roster ([4f65e79](https://github.com/elct9620/kobako/commit/4f65e79c3aa37ed1fc47a5002c8a567c3df8cc84))
|
|
32
|
+
* **tasks:** hold the tier roster to the repo and share it across instruments ([9a75586](https://github.com/elct9620/kobako/commit/9a755866236c134ea7601006211338eefae0a348))
|
|
33
|
+
* **tasks:** inventory exported macros in the pub-surface scan ([e4457ea](https://github.com/elct9620/kobako/commit/e4457eafecfa24eb5fc9aade086f2be9b0862818))
|
|
34
|
+
* **tasks:** measure the tooling tiers in stats and hotspots ([8def9ff](https://github.com/elct9620/kobako/commit/8def9ff96d36c2020e04d7e9bb0d9cd3fdad3e43))
|
|
35
|
+
* **tasks:** pin the tier roster to the repo from both sides ([3b7a154](https://github.com/elct9620/kobako/commit/3b7a154ebbfe8629392a81599df37d136a0eec99))
|
|
36
|
+
* **tasks:** rank churn x size x fan-in hotspots since the last release ([73b3bc8](https://github.com/elct9620/kobako/commit/73b3bc8fdee19f8c09a91ccf96348688ea3ccd1c))
|
|
37
|
+
* **tasks:** read the anchor citation profile back as an instrument ([47961cb](https://github.com/elct9620/kobako/commit/47961cb689d9c681f5c697af7e6bca52a4c7817f))
|
|
38
|
+
* **tasks:** report pub items no in-repo downstream consumes ([3eb0481](https://github.com/elct9620/kobako/commit/3eb048199a0e2e41509513750bec4fcbc6b7cc69))
|
|
39
|
+
* **tasks:** resolve the SPEC-local F/J/N families in the anchors gate ([33d3118](https://github.com/elct9620/kobako/commit/33d31181be3a800a4cc3d4eb32324d840473401f))
|
|
40
|
+
* **tasks:** scan pub statics in the pub-surface instrument ([72cff5e](https://github.com/elct9620/kobako/commit/72cff5ef369a73c353ecdd25c1d4688ec8dd2f82))
|
|
41
|
+
* **tasks:** score hotspots on impl-only lines for Rust files ([704b419](https://github.com/elct9620/kobako/commit/704b41971eac9e284b570bd0ac2fdd9af656bbca))
|
|
42
|
+
* **tasks:** seat anchors:coverage and wire:symmetry in the release gate ([ab035bb](https://github.com/elct9620/kobako/commit/ab035bb3c4e049e6319d33cdaf1d715d4e83eedb))
|
|
43
|
+
* **tasks:** widen the fan-in and ext-code scans to their whole tiers ([43d2bad](https://github.com/elct9620/kobako/commit/43d2badfea3c13cb062e67d5ded8410a339862d3))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Bug Fixes
|
|
47
|
+
|
|
48
|
+
* **codec:** reject ext 0x02 anywhere in the Panic frame, not only details ([062e29d](https://github.com/elct9620/kobako/commit/062e29d6ee15264e1bd942502b751cfe7610acad))
|
|
49
|
+
* **codec:** reject the Fault envelope in Rust host payload positions ([bdf2ed7](https://github.com/elct9620/kobako/commit/bdf2ed78fde2798bdc15f4e969bda228cf482f4b))
|
|
50
|
+
* **codec:** reject the reserved Handle id 0 on the Rust wire tier ([5f7e482](https://github.com/elct9620/kobako/commit/5f7e4821680e553da355d5257b0619e4a1cdce72))
|
|
51
|
+
* **tasks:** fail the anchors gate when no SPEC ceiling parses ([8d185f5](https://github.com/elct9620/kobako/commit/8d185f59afc28b5334d934f6916fd64e458e9359))
|
|
52
|
+
* **tasks:** inventory every codec-bearing class per transport file ([e553ae1](https://github.com/elct9620/kobako/commit/e553ae1578245d9bfca63cf4e8429666857dbb92))
|
|
53
|
+
* **tasks:** keep tooling-suite fixture tokens out of the anchor scans ([58a78b9](https://github.com/elct9620/kobako/commit/58a78b990684d4ad87d4db18b21eb8bd1c98713a))
|
|
54
|
+
* **tasks:** mark unmeasured fan-in as '-' in the hotspot report ([a4959fe](https://github.com/elct9620/kobako/commit/a4959fe0c93546711dd16002e38d993e350bcb8f))
|
|
55
|
+
* **tasks:** read pub fn qualifiers in the pub-surface scan ([ac0e7d3](https://github.com/elct9620/kobako/commit/ac0e7d3030729ad981b6a8de504c5837daf2038d))
|
|
56
|
+
* **tasks:** read the codec-bearing class in the wire-symmetry inventory ([85ce937](https://github.com/elct9620/kobako/commit/85ce937fdb35953b26fee5fa4ee55f53988147ea))
|
|
57
|
+
* **tasks:** scan the whole tier in every wire-symmetry inventory ([c2bba5c](https://github.com/elct9620/kobako/commit/c2bba5c02ca20dca14d061a03f8f4581d0995a66))
|
|
58
|
+
* **tasks:** truncate the pub-surface scan only at the test module ([9c6dc05](https://github.com/elct9620/kobako/commit/9c6dc054bfa1fe79c3410fa794290a1ce5b85f4d))
|
|
59
|
+
* **test:** flunk instead of skip when CI lacks a built guest prerequisite ([f7818f6](https://github.com/elct9620/kobako/commit/f7818f68c8114e5f9b0012a4dd4b069695bc3e32))
|
|
60
|
+
* **transport:** refuse the Fault envelope outside its legal position (E-50) ([58c5069](https://github.com/elct9620/kobako/commit/58c50691d32e245f41c2f8041dfb7e92b05b80e9))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Performance Improvements
|
|
64
|
+
|
|
65
|
+
* **bench:** re-bless the anchor onto the wasmtime 46 round ([9225702](https://github.com/elct9620/kobako/commit/9225702106c0c4caf90425ac08c11452b6388b55))
|
|
66
|
+
* **dispatch:** skip the Handle walk when the request carried none ([cd63514](https://github.com/elct9620/kobako/commit/cd6351459c492084a401fd1b6a8e4293761de93f))
|
|
67
|
+
* **sandbox:** skip the Handle walk when the invocation result carried none ([d2c4947](https://github.com/elct9620/kobako/commit/d2c494782d316df3bbf5fc223a8beaa1f04f5718))
|
|
68
|
+
* **transport:** skip the Handle walk when the yield result carried none ([bca463b](https://github.com/elct9620/kobako/commit/bca463bce0b8913941b3e3ba11c31cb41b843545))
|
|
69
|
+
|
|
3
70
|
## [0.14.0](https://github.com/elct9620/kobako/compare/v0.13.0...v0.14.0) (2026-07-08)
|
|
4
71
|
|
|
5
72
|
|
data/Cargo.lock
CHANGED
|
@@ -929,7 +929,7 @@ dependencies = [
|
|
|
929
929
|
|
|
930
930
|
[[package]]
|
|
931
931
|
name = "kobako"
|
|
932
|
-
version = "0.
|
|
932
|
+
version = "0.16.0"
|
|
933
933
|
dependencies = [
|
|
934
934
|
"kobako-runtime",
|
|
935
935
|
"kobako-wasmtime",
|
|
@@ -938,11 +938,11 @@ dependencies = [
|
|
|
938
938
|
|
|
939
939
|
[[package]]
|
|
940
940
|
name = "kobako-runtime"
|
|
941
|
-
version = "0.
|
|
941
|
+
version = "0.10.0"
|
|
942
942
|
|
|
943
943
|
[[package]]
|
|
944
944
|
name = "kobako-wasmtime"
|
|
945
|
-
version = "0.
|
|
945
|
+
version = "0.10.0"
|
|
946
946
|
dependencies = [
|
|
947
947
|
"kobako-runtime",
|
|
948
948
|
"libc",
|
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, so you can execute untrusted Ruby scripts (LLM-generated code, user formulas, student submissions, third-party plugins) in-process without giving them access to host memory, files, network, or credentials.
|
|
5
|
+
Kobako is a Ruby gem that embeds a Wasm-isolated mruby interpreter inside your application, so you can execute untrusted Ruby scripts (LLM-generated code, user formulas, student submissions, third-party plugins) in-process without giving them 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. The only way a guest script can reach the outside world is through Host App-declared **Services** — named
|
|
7
|
+
The host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby and a Transport proxy. The only way a guest script can reach the outside world is through Host App-declared **Services** — 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
|
|
@@ -39,27 +39,102 @@ gem install kobako
|
|
|
39
39
|
|
|
40
40
|
## Quick Start
|
|
41
41
|
|
|
42
|
+
The same untrusted script runs identically from either host frontend. It executes inside the Wasm guest and cannot read your filesystem, open sockets, or touch your environment.
|
|
43
|
+
|
|
42
44
|
```ruby
|
|
43
45
|
require "kobako"
|
|
44
46
|
|
|
45
47
|
sandbox = Kobako::Sandbox.new
|
|
48
|
+
sandbox.eval("1 + 2") # => 3
|
|
49
|
+
```
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
```rust
|
|
52
|
+
use kobako::{Options, Sandbox};
|
|
53
|
+
|
|
54
|
+
// Options::default() is secure by default: no caps, hermetic isolation.
|
|
55
|
+
let mut sandbox = Sandbox::new("kobako.wasm", Options::default())?;
|
|
56
|
+
sandbox.eval("1 + 2")?; // => Value::Int(3)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
## Frontends & Guest Binaries
|
|
62
|
+
|
|
63
|
+
Embedding kobako is two independent choices: the **host frontend** you build against, and the **Guest Binary** it runs. They compose freely — any frontend loads any Guest Binary, so a Ruby host can run a JSON-enabled guest and a Rust host can run the pure default.
|
|
64
|
+
|
|
65
|
+
### Host frontends
|
|
66
|
+
|
|
67
|
+
The host embeds the sandbox and owns the SPEC wire codec. Choose by your host language and how much of the contract you want pre-assembled.
|
|
68
|
+
|
|
69
|
+
| Frontend | Package | Add it | Best for |
|
|
70
|
+
|----------|---------|--------|----------|
|
|
71
|
+
| Ruby gem | `kobako` (RubyGems) | `gem install kobako` | A Ruby host — Services, Handles, snippets, and pooling out of the box |
|
|
72
|
+
| Rust SDK | `kobako` (crates.io) | `kobako = "0.9"` | A Rust host — the same behavior contract behind an idiomatic Rust API |
|
|
73
|
+
| Low-level crates | `kobako-wasmtime` + `kobako-runtime` + `kobako-codec` | Cargo deps | A custom host, or driving the wire directly in another language |
|
|
74
|
+
|
|
75
|
+
The Rust crates are documented on [crates.io](https://crates.io/crates/kobako); the Ruby gem is this README. Two 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.
|
|
76
|
+
|
|
77
|
+
### Pre-built Guest Binaries
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
| Variant | File | Adds | Distribution |
|
|
82
|
+
|---------|------|------|--------------|
|
|
83
|
+
| Pure (default) | `kobako.wasm` | mruby + IO | Bundled in the gem, and a Release asset |
|
|
84
|
+
| +regexp | `kobako+regexp.wasm` | Regexp / MatchData (ASCII) | Release asset |
|
|
85
|
+
| +regexp-unicode | `kobako+regexp-unicode.wasm` | Regexp / MatchData (Unicode) | Release asset |
|
|
86
|
+
| +json | `kobako+json.wasm` | JSON | Release asset |
|
|
87
|
+
| +full | `kobako+full.wasm` | JSON + Regexp (ASCII) | Release asset |
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
sandbox = Kobako::Sandbox.new(wasm_path: "kobako+full.wasm")
|
|
91
|
+
sandbox.eval('JSON.generate({ n: "42".to_i })') # => "{\"n\":42}"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Custom Guest Binaries
|
|
95
|
+
|
|
96
|
+
When no pre-built variant matches your capability set, assemble a guest in Rust. `kobako-mruby` is the harness; its `init_gems` hook is where you install exactly the capability gems you want — the shipped `kobako-io` / `kobako-regexp` / `kobako-json`, or your own `beni::Gem`. `kobako-core`'s `export_guest!` emits the wasm ABI. `wasm/kobako-wasm/src/guest.rs` is the worked example.
|
|
97
|
+
|
|
98
|
+
| Guest crate | Role |
|
|
99
|
+
|-------------|------|
|
|
100
|
+
| `kobako-mruby` | mruby guest harness — the `MrbGuest` trait and provided flows |
|
|
101
|
+
| `kobako-core` | Guest ABI contract — the `Guest` trait and the `export_guest!` macro |
|
|
102
|
+
| `kobako-io` | IO / Kernel capability gem |
|
|
103
|
+
| `kobako-regexp` | Regexp / MatchData capability gem |
|
|
104
|
+
| `kobako-json` | JSON capability gem |
|
|
105
|
+
|
|
106
|
+
```rust
|
|
107
|
+
use beni::{Error, Mrb};
|
|
108
|
+
|
|
109
|
+
struct MyGuest;
|
|
110
|
+
|
|
111
|
+
// Pick the capability gems the guest exposes.
|
|
112
|
+
impl kobako_mruby::MrbGuest for MyGuest {
|
|
113
|
+
fn init_gems(mrb: &Mrb) -> Result<(), Error> {
|
|
114
|
+
mrb.init_gem::<kobako_io::KobakoIo>()?;
|
|
115
|
+
mrb.init_gem::<kobako_json::KobakoJson>()?;
|
|
116
|
+
Ok(())
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Forward the ABI contract to the harness flows.
|
|
121
|
+
impl kobako_core::Guest for MyGuest {
|
|
122
|
+
fn eval() { <MyGuest as kobako_mruby::MrbGuest>::eval() }
|
|
123
|
+
fn run(env: &[u8]) { <MyGuest as kobako_mruby::MrbGuest>::run(env) }
|
|
124
|
+
fn yield_to_block(req: &[u8]) -> u64 { <MyGuest as kobako_mruby::MrbGuest>::yield_to_block(req) }
|
|
125
|
+
}
|
|
50
126
|
|
|
51
|
-
|
|
127
|
+
kobako_core::export_guest!(MyGuest);
|
|
52
128
|
```
|
|
53
129
|
|
|
54
|
-
|
|
130
|
+
Build the crate as a `cdylib` for `wasm32-wasip1`, then bake the canonical boot state into the artifact (see [`CLAUDE.md`](CLAUDE.md) § Build Pipeline).
|
|
55
131
|
|
|
56
132
|
## Glossary
|
|
57
133
|
|
|
58
134
|
| Term | Meaning |
|
|
59
135
|
|------|---------|
|
|
60
136
|
| Sandbox | The runtime unit (`Kobako::Sandbox`) that runs guest code and returns a result or raises a typed error. |
|
|
61
|
-
| Service | A host
|
|
62
|
-
| Namespace / Member | A guest-visible Ruby module, and a named binding (a module constant) within it. |
|
|
137
|
+
| Service | A host object bound at a constant-path name (`MyService::KV`) — the guest's only path to host resources. |
|
|
63
138
|
| Invocation | One `#eval` or `#run`; capability state resets between invocations. |
|
|
64
139
|
| Snippet | Named mruby code (source or bytecode) replayed into a fresh state before every invocation. |
|
|
65
140
|
| Handle | An opaque token the guest holds for a host object the wire cannot transmit directly. |
|
|
@@ -69,7 +144,7 @@ The script executes inside the Wasm guest. It cannot read your filesystem, open
|
|
|
69
144
|
|
|
70
145
|
### Services
|
|
71
146
|
|
|
72
|
-
|
|
147
|
+
`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 and invokes its public methods through the Transport wire. See [`docs/behavior/registration.md`](docs/behavior/registration.md) B-08..B-12.
|
|
73
148
|
|
|
74
149
|
```ruby
|
|
75
150
|
class User
|
|
@@ -80,8 +155,8 @@ class User
|
|
|
80
155
|
end
|
|
81
156
|
end
|
|
82
157
|
|
|
83
|
-
sandbox.
|
|
84
|
-
sandbox.
|
|
158
|
+
sandbox.bind("Project::User", User.new(name: "alice"))
|
|
159
|
+
sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
|
|
85
160
|
|
|
86
161
|
sandbox.eval(<<~RUBY)
|
|
87
162
|
Project::User.name # => "alice"
|
|
@@ -89,7 +164,7 @@ sandbox.eval(<<~RUBY)
|
|
|
89
164
|
RUBY
|
|
90
165
|
```
|
|
91
166
|
|
|
92
|
-
|
|
167
|
+
Each `::`-separated path segment must match `/\A[A-Z]\w*\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`.
|
|
93
168
|
|
|
94
169
|
### Output Capture
|
|
95
170
|
|
|
@@ -164,7 +239,7 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
164
239
|
───────────── setup phase (mutable) ─────────────
|
|
165
240
|
|
|
166
241
|
sandbox = Kobako::Sandbox.new
|
|
167
|
-
sandbox.
|
|
242
|
+
sandbox.bind("KV::Lookup", ...)
|
|
168
243
|
sandbox.preload(code: ..., name: :Adder)
|
|
169
244
|
sandbox.preload(code: ..., name: :Greeter)
|
|
170
245
|
|
|
@@ -173,7 +248,7 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
173
248
|
|
|
174
249
|
═════════════════ seal point ═════════════════
|
|
175
250
|
First #eval or #run freezes the Service registry
|
|
176
|
-
and snippet table. Further
|
|
251
|
+
and snippet table. Further bind / preload now
|
|
177
252
|
raise ArgumentError.
|
|
178
253
|
|
|
179
254
|
│
|
|
@@ -188,7 +263,7 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
188
263
|
:Adder → defines Adder
|
|
189
264
|
:Greeter → defines Greeter
|
|
190
265
|
|
|
191
|
-
3. dispatch: eval(source) or run(:Target, *args)
|
|
266
|
+
3. dispatch: eval(source) or run(:Target, *args, **kwargs)
|
|
192
267
|
|
|
193
268
|
4. return value to host
|
|
194
269
|
|
|
@@ -206,11 +281,11 @@ For workloads that must be isolated from each other (one Sandbox per tenant, per
|
|
|
206
281
|
|
|
207
282
|
For hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes and hands each one to a single exclusive holder at a time ([`docs/behavior/runtime.md`](docs/behavior/runtime.md) B-46..B-48). Construction forwards every `Sandbox.new` keyword verbatim; the optional block is the per-Sandbox setup window and runs exactly once per constructed Sandbox.
|
|
208
283
|
|
|
209
|
-
`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. B-49 bakes the shared boot state into the artifact and every dynamic script still compiles and runs per invocation, so all a pool actually saves is the ~
|
|
284
|
+
`Kobako::Pool` is experimental today and is best treated as a convenience for warm, pre-configured reuse rather than a throughput optimisation. B-49 bakes the shared boot state into the artifact and every dynamic script still compiles and runs per invocation, so all a pool actually saves is the ~28 µs host-side `Sandbox.new`. For the workload kobako is built for — many small, short-lived Sandboxes running dynamic scripts — that is not a significant gain (~4-5% in the [serverless example](examples/serverless/README.md), and proportionally less once the script itself does real work).
|
|
210
285
|
|
|
211
286
|
```ruby
|
|
212
287
|
pool = Kobako::Pool.new(slots: 4) do |sandbox|
|
|
213
|
-
sandbox.
|
|
288
|
+
sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
|
|
214
289
|
end
|
|
215
290
|
|
|
216
291
|
pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))) }
|
|
@@ -228,7 +303,7 @@ Sandboxes construct lazily on first demand. `#with` yields a Sandbox with empty
|
|
|
228
303
|
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. See [`docs/behavior/yield.md`](docs/behavior/yield.md) B-23..B-30.
|
|
229
304
|
|
|
230
305
|
```ruby
|
|
231
|
-
sandbox.
|
|
306
|
+
sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
|
|
232
307
|
|
|
233
308
|
sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }')
|
|
234
309
|
# => [2, 4, 6]
|
|
@@ -244,13 +319,13 @@ class Greeter
|
|
|
244
319
|
def greet = "hi, #{@name}"
|
|
245
320
|
end
|
|
246
321
|
|
|
247
|
-
sandbox.
|
|
322
|
+
sandbox.bind("Factory::Make", ->(name) { Greeter.new(name) })
|
|
248
323
|
|
|
249
324
|
sandbox.eval('Factory::Make.call("Bob").greet') # => "hi, Bob" (Handle round-trip inside guest)
|
|
250
325
|
sandbox.eval('Factory::Make.call("Bob")') # => #<Greeter @name="Bob"> (B-37 restoration)
|
|
251
326
|
```
|
|
252
327
|
|
|
253
|
-
A `break` value from a guest block is the one exception: it unwinds back to the guest
|
|
328
|
+
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.
|
|
254
329
|
|
|
255
330
|
Each dispatch that hands back a non-wire-representable object allocates a *new* Handle — kobako never deduplicates by object identity (B-15, B-17). 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:
|
|
256
331
|
|
|
@@ -319,7 +394,7 @@ class ThemeReader # only #color is reachable; AppConfig.secret_key is n
|
|
|
319
394
|
end
|
|
320
395
|
|
|
321
396
|
sandbox = Kobako::Sandbox.new
|
|
322
|
-
sandbox.
|
|
397
|
+
sandbox.bind("Cfg::Settings", ThemeReader.new) # not: bind("Cfg::Settings", AppConfig)
|
|
323
398
|
|
|
324
399
|
sandbox.eval('Cfg::Settings.color') # => "#3366ff" — every other method raises NoMethodError
|
|
325
400
|
```
|
|
@@ -329,7 +404,7 @@ place: a private `respond_to_guest?(name)` answers, per method, whether the gues
|
|
|
329
404
|
it. Returning `false` for every name makes the object opaque — a credential the guest
|
|
330
405
|
forwards to another Service but never reads — while a named subset becomes an allow-list.
|
|
331
406
|
|
|
332
|
-
Guest code can name any
|
|
407
|
+
Guest code can name any `MyService::KV` path, but a forged name only resolves to
|
|
333
408
|
something you bound — the real authorization gate is this host-side allowlist. Give each
|
|
334
409
|
trust context its own Sandbox, and see [`docs/security-model.md`](docs/security-model.md) for the rest
|
|
335
410
|
as security-design concerns: validating untrusted input, default-deny external effects,
|
|
@@ -342,15 +417,15 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
|
|
|
342
417
|
| Phase | Cost |
|
|
343
418
|
|--------------------------------------------------------------|-----------------------|
|
|
344
419
|
| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
|
|
345
|
-
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~
|
|
346
|
-
| Subsequent `Sandbox.new` (caches warm) | ~
|
|
347
|
-
| Warm `#eval("nil")` on a reused Sandbox | ~
|
|
348
|
-
| Warm `#run(:Entrypoint, ...)` dispatch | ~
|
|
420
|
+
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~3 ms one-time |
|
|
421
|
+
| Subsequent `Sandbox.new` (caches warm) | ~28 µs |
|
|
422
|
+
| Warm `#eval("nil")` on a reused Sandbox | ~71 µs |
|
|
423
|
+
| Warm `#run(:Entrypoint, ...)` dispatch | ~97 µs |
|
|
349
424
|
| Service call amortized inside one invocation | ~6.8 µs |
|
|
350
|
-
| Snippet replay per invocation | ~
|
|
425
|
+
| Snippet replay per invocation | ~7.6 µs each |
|
|
351
426
|
| Per additional idle Sandbox (RSS) | ~1 KB |
|
|
352
427
|
|
|
353
|
-
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 (the canonical boot state is baked into the artifact and instantiated per invocation), which is why a thousand idle tenants cost ~
|
|
428
|
+
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 (the canonical boot state is baked into the artifact and instantiated per invocation), which is why a thousand idle tenants cost ~33 MB total. `ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput stays around 17k `#eval`/s regardless of Thread count, though Ruby-side `#eval` setup still overlaps. A +10% regression on any of the six SPEC-mandated benchmarks blocks release.
|
|
354
429
|
|
|
355
430
|
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 (`#10` in [`benchmark/README.md`](benchmark/README.md)). There `=~` (~5 µs/match) costs about 4× `match?` (~1.2 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.
|
|
356
431
|
|
data/ROADMAP.md
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
kobako is a Ruby gem providing an in-process Wasm sandbox for untrusted mruby
|
|
4
4
|
code: a wasmtime host runs a precompiled `kobako.wasm` guest, with host↔guest
|
|
5
5
|
Transport over a MessagePack wire. Features cover one-shot `#eval`, preload +
|
|
6
|
-
`#run` dispatch, Service injection
|
|
6
|
+
`#run` dispatch, Service injection at constant-path names, opaque
|
|
7
7
|
Capability Handles, block yield re-entry, three-class error attribution,
|
|
8
8
|
output capture, and a warm Sandbox pool.
|
|
9
9
|
|
|
10
10
|
| Feature | Entry Points | Notes |
|
|
11
11
|
|---------|-------------|-------|
|
|
12
12
|
| ✅ [F-01 Sandbox instantiation](docs/behavior/lifecycle.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | B-22 (per-Thread isolation) is exercised only indirectly through the pool contention tests; no test cites it |
|
|
13
|
-
| ✅ [F-02
|
|
14
|
-
| ✅ [F-03 Member binding](docs/behavior/registration.md) | [lib/kobako/namespace.rb](lib/kobako/namespace.rb) | — |
|
|
13
|
+
| ✅ [F-02 Service binding](docs/behavior/registration.md) | [lib/kobako/catalog/services.rb](lib/kobako/catalog/services.rb) | — |
|
|
15
14
|
| ✅ [F-04 Synchronous mruby source execution (`#eval`)](docs/behavior/lifecycle.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
|
|
16
15
|
| ✅ [F-05 Guest-initiated Transport dispatch](docs/behavior/dispatch.md) | [lib/kobako/transport/dispatcher.rb](lib/kobako/transport/dispatcher.rb) | — |
|
|
17
16
|
| ✅ [F-06 Capability Handle encoding and referencing](docs/behavior/dispatch.md) | [lib/kobako/catalog/handles.rb](lib/kobako/catalog/handles.rb) | — |
|
data/SECURITY.md
CHANGED
|
@@ -25,7 +25,7 @@ ask to stay anonymous.
|
|
|
25
25
|
|
|
26
26
|
In scope is anything that lets guest code cross the isolation boundary it should not:
|
|
27
27
|
reaching host memory, the filesystem, the network, or `ENV`; obtaining ambient time or
|
|
28
|
-
entropy the host froze; reaching a
|
|
28
|
+
entropy the host froze; reaching a Service you never bound; or a
|
|
29
29
|
memory-safety fault in the host codec or wasmtime driver.
|
|
30
30
|
|
|
31
31
|
Out of scope is what a bound Service is *designed* to expose: if guest code reaches a
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.9.0...kobako-runtime-v0.10.0) (2026-07-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
10
|
+
## [0.9.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.8.0...kobako-runtime-v0.9.0) (2026-07-11)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Miscellaneous Chores
|
|
14
|
+
|
|
15
|
+
* **kobako-runtime:** Synchronize kobako crates versions
|
|
16
|
+
|
|
3
17
|
## [0.8.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.7.0...kobako-runtime-v0.8.0) (2026-07-08)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
use crate::error::Trap;
|
|
10
10
|
|
|
11
11
|
/// One captured output channel: the bytes the guest wrote (already clipped
|
|
12
|
-
/// to the channel's cap) and whether the cap was reached.
|
|
12
|
+
/// to the channel's cap) and whether the cap was reached. `Default` is the
|
|
13
|
+
/// pre-invocation sentinel — no bytes, cap not reached — shared by every
|
|
14
|
+
/// frontend's before-first-invocation readout.
|
|
15
|
+
#[derive(Default)]
|
|
13
16
|
pub struct Capture {
|
|
14
17
|
pub bytes: Vec<u8>,
|
|
15
18
|
pub truncated: bool,
|
|
@@ -27,8 +30,9 @@ pub enum Completion {
|
|
|
27
30
|
/// Resource usage of one guest invocation, measured across the same
|
|
28
31
|
/// bracket as the wall-clock / memory caps: `wall_time` is the seconds
|
|
29
32
|
/// spent inside the guest export call; `memory_peak` is the high-water
|
|
30
|
-
/// `memory.grow` delta in bytes past the entry-time baseline.
|
|
31
|
-
|
|
33
|
+
/// `memory.grow` delta in bytes past the entry-time baseline. `Default`
|
|
34
|
+
/// (all zeros) is the pre-invocation sentinel.
|
|
35
|
+
#[derive(Clone, Copy, Default)]
|
|
32
36
|
pub struct Usage {
|
|
33
37
|
pub wall_time: f64,
|
|
34
38
|
pub memory_peak: usize,
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.9.0...kobako-wasmtime-v0.10.0) (2026-07-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Miscellaneous Chores
|
|
7
|
+
|
|
8
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
9
|
+
|
|
10
|
+
## [0.9.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.8.0...kobako-wasmtime-v0.9.0) (2026-07-11)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Miscellaneous Chores
|
|
14
|
+
|
|
15
|
+
* **kobako-wasmtime:** Synchronize kobako crates versions
|
|
16
|
+
|
|
3
17
|
## [0.8.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.7.0...kobako-wasmtime-v0.8.0) (2026-07-08)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
[package]
|
|
17
17
|
name = "kobako-wasmtime"
|
|
18
|
-
version = "0.
|
|
18
|
+
version = "0.10.0"
|
|
19
19
|
edition = "2021"
|
|
20
20
|
description = "wasmtime implementation of the kobako host runtime contract."
|
|
21
21
|
license = "Apache-2.0"
|
|
@@ -28,7 +28,7 @@ categories = ["wasm", "virtualization"]
|
|
|
28
28
|
# The engine-neutral contract this crate implements. The version pin
|
|
29
29
|
# rides the linked release group; the path keeps in-tree builds (and
|
|
30
30
|
# the Ruby gem, which ships both crates) resolving locally.
|
|
31
|
-
kobako-runtime = { version = "0.
|
|
31
|
+
kobako-runtime = { version = "0.10.0", path = "../kobako-runtime" }
|
|
32
32
|
# wasmtime — host-side embedder for kobako.wasm. We disable default-features
|
|
33
33
|
# and opt back in only what kobako needs: a Cranelift-backed runtime that can
|
|
34
34
|
# compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
|
|
@@ -18,15 +18,19 @@ pub(crate) fn pipe_capacity(cap: Option<usize>) -> usize {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/// Pure
|
|
22
|
-
/// pipe snapshot
|
|
23
|
-
///
|
|
24
|
-
///
|
|
25
|
-
///
|
|
26
|
-
///
|
|
27
|
-
|
|
21
|
+
/// Pure clipping core shared by the snapshot readback: given the
|
|
22
|
+
/// unclipped pipe snapshot (owned — truncated in place, so the readback
|
|
23
|
+
/// costs one copy out of the pipe, not two), return the bytes Ruby
|
|
24
|
+
/// should observe plus the truncation flag. `truncated` is `true` only
|
|
25
|
+
/// when the snapshot strictly exceeded the cap — this is the "wrote
|
|
26
|
+
/// `cap + 1` bytes into a `cap + 1`-sized pipe" case; "wrote exactly
|
|
27
|
+
/// `cap` bytes" stays `false`.
|
|
28
|
+
pub(crate) fn clip_capture(mut raw: Vec<u8>, cap: Option<usize>) -> (Vec<u8>, bool) {
|
|
28
29
|
match cap {
|
|
29
|
-
Some(c) if raw.len() > c =>
|
|
30
|
+
Some(c) if raw.len() > c => {
|
|
31
|
+
raw.truncate(c);
|
|
32
|
+
(raw, true)
|
|
33
|
+
}
|
|
30
34
|
_ => (raw, false),
|
|
31
35
|
}
|
|
32
36
|
}
|
|
@@ -53,14 +57,14 @@ mod tests {
|
|
|
53
57
|
|
|
54
58
|
#[test]
|
|
55
59
|
fn clip_capture_returns_full_bytes_when_under_cap() {
|
|
56
|
-
let (bytes, truncated) = clip_capture(b"abc", Some(5));
|
|
60
|
+
let (bytes, truncated) = clip_capture(b"abc".to_vec(), Some(5));
|
|
57
61
|
assert_eq!(bytes, b"abc");
|
|
58
62
|
assert!(!truncated);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
#[test]
|
|
62
66
|
fn clip_capture_does_not_flag_truncation_at_exactly_cap_bytes() {
|
|
63
|
-
let (bytes, truncated) = clip_capture(b"abcde", Some(5));
|
|
67
|
+
let (bytes, truncated) = clip_capture(b"abcde".to_vec(), Some(5));
|
|
64
68
|
assert_eq!(bytes, b"abcde");
|
|
65
69
|
assert!(!truncated);
|
|
66
70
|
}
|
|
@@ -70,21 +74,21 @@ mod tests {
|
|
|
70
74
|
// The pipe is sized `cap + 1`, so the snapshot can be at most
|
|
71
75
|
// 6 bytes when `cap == 5`; that surface is what triggers the
|
|
72
76
|
// truncation flag.
|
|
73
|
-
let (bytes, truncated) = clip_capture(b"abcdef", Some(5));
|
|
77
|
+
let (bytes, truncated) = clip_capture(b"abcdef".to_vec(), Some(5));
|
|
74
78
|
assert_eq!(bytes, b"abcde");
|
|
75
79
|
assert!(truncated);
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
#[test]
|
|
79
83
|
fn clip_capture_treats_none_as_uncapped() {
|
|
80
|
-
let (bytes, truncated) = clip_capture(b"abcdef", None);
|
|
84
|
+
let (bytes, truncated) = clip_capture(b"abcdef".to_vec(), None);
|
|
81
85
|
assert_eq!(bytes, b"abcdef");
|
|
82
86
|
assert!(!truncated);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
#[test]
|
|
86
90
|
fn clip_capture_handles_empty_input() {
|
|
87
|
-
let (bytes, truncated) = clip_capture(
|
|
91
|
+
let (bytes, truncated) = clip_capture(Vec::new(), Some(5));
|
|
88
92
|
assert_eq!(bytes, b"");
|
|
89
93
|
assert!(!truncated);
|
|
90
94
|
}
|
|
@@ -203,19 +203,18 @@ impl Driver {
|
|
|
203
203
|
wall_time: data.wall_time().as_secs_f64(),
|
|
204
204
|
memory_peak: data.memory_peak(),
|
|
205
205
|
};
|
|
206
|
-
let (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
capture::clip_capture(&stderr_raw, self.config.stderr_limit_bytes);
|
|
206
|
+
let (stdout_bytes, stdout_truncated) =
|
|
207
|
+
capture::clip_capture(data.stdout_bytes(), self.config.stdout_limit_bytes);
|
|
208
|
+
let (stderr_bytes, stderr_truncated) =
|
|
209
|
+
capture::clip_capture(data.stderr_bytes(), self.config.stderr_limit_bytes);
|
|
211
210
|
Snapshot {
|
|
212
211
|
completion,
|
|
213
212
|
stdout: Capture {
|
|
214
|
-
bytes:
|
|
213
|
+
bytes: stdout_bytes,
|
|
215
214
|
truncated: stdout_truncated,
|
|
216
215
|
},
|
|
217
216
|
stderr: Capture {
|
|
218
|
-
bytes:
|
|
217
|
+
bytes: stderr_bytes,
|
|
219
218
|
truncated: stderr_truncated,
|
|
220
219
|
},
|
|
221
220
|
usage,
|