kobako 0.14.0-aarch64-linux → 0.15.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +51 -0
  4. data/README.md +104 -29
  5. data/ROADMAP.md +2 -3
  6. data/SECURITY.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +7 -0
  8. data/crates/kobako-runtime/README.md +1 -1
  9. data/crates/kobako-wasmtime/CHANGELOG.md +7 -0
  10. data/crates/kobako-wasmtime/README.md +1 -1
  11. data/data/kobako.wasm +0 -0
  12. data/lib/kobako/3.3/kobako.so +0 -0
  13. data/lib/kobako/3.4/kobako.so +0 -0
  14. data/lib/kobako/4.0/kobako.so +0 -0
  15. data/lib/kobako/catalog/handles.rb +1 -1
  16. data/lib/kobako/catalog/services.rb +123 -0
  17. data/lib/kobako/catalog/snippets.rb +1 -1
  18. data/lib/kobako/catalog.rb +2 -2
  19. data/lib/kobako/codec/decoder.rb +3 -3
  20. data/lib/kobako/codec/encoder.rb +4 -4
  21. data/lib/kobako/codec/ext_types.rb +169 -0
  22. data/lib/kobako/codec/state.rb +98 -0
  23. data/lib/kobako/codec/utils.rb +2 -2
  24. data/lib/kobako/codec.rb +23 -6
  25. data/lib/kobako/fault.rb +1 -1
  26. data/lib/kobako/handle.rb +1 -1
  27. data/lib/kobako/outcome.rb +13 -7
  28. data/lib/kobako/pool.rb +1 -1
  29. data/lib/kobako/sandbox.rb +20 -13
  30. data/lib/kobako/transport/dispatcher.rb +25 -18
  31. data/lib/kobako/transport/request.rb +14 -9
  32. data/lib/kobako/transport/response.rb +9 -2
  33. data/lib/kobako/transport/yield.rb +4 -1
  34. data/lib/kobako/transport/yielder.rb +14 -6
  35. data/lib/kobako/version.rb +1 -1
  36. data/sig/kobako/catalog/services.rbs +25 -0
  37. data/sig/kobako/codec/ext_types.rbs +31 -0
  38. data/sig/kobako/codec/state.rbs +20 -0
  39. data/sig/kobako/codec.rbs +3 -0
  40. data/sig/kobako/sandbox.rbs +1 -1
  41. data/sig/kobako/transport/dispatcher.rbs +4 -4
  42. data/sig/kobako/transport/yielder.rbs +1 -1
  43. data/sig/kobako/transport.rbs +2 -2
  44. metadata +8 -8
  45. data/lib/kobako/catalog/namespaces.rb +0 -115
  46. data/lib/kobako/codec/factory.rb +0 -187
  47. data/lib/kobako/namespace.rb +0 -78
  48. data/sig/kobako/catalog/namespaces.rbs +0 -17
  49. data/sig/kobako/codec/factory.rbs +0 -34
  50. data/sig/kobako/namespace.rbs +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f52be5d778b482a65c810fb530ac716fd2232d2b54cc98aea60f906aa6959fe9
4
- data.tar.gz: f537ac1f62dad56b63bbed4456b548495310c6809983765005d54f54ed195e4d
3
+ metadata.gz: 00df75af1fd85b3c4d3729d456148a282ab904f26162e6930ecd8ad003bcd9df
4
+ data.tar.gz: 34533660556ce44eddbc76831b973ed10217e5a3801affbb5b7a991e15069c3a
5
5
  SHA512:
6
- metadata.gz: b0e38dd483517aa63e12d90e1936ee7f27ea0a3c2c1f7a998f7e475b4351313f170284d56c6b9e33f30d7925659adf5ed509b2c3b3f6eeaa8a8ae75bd25d620d
7
- data.tar.gz: 2269f4eac755583383a491a9441cd2b30f54e174c7b16187ae0ef9e9520bd69dffd8950061aa59a387e9e91360d9c8bfdc21232f1da746cb3385b831ad723adb
6
+ metadata.gz: 34250f5db8642efbab2654993df69316be3cdb581bafacc07ac6993e072ea5addf3c04bae86d41ad5412c182b3f22e9aa158b9e9cb27d86571a7cf01ad38a674
7
+ data.tar.gz: ac5d4d68b447714f78412acd554c842cbd52f8bfab46794ffa7fabe1dcc6a169bb4d04a3caf10e88bf16ff41b4312ab30626687ee46b31530514b5fa4ac93242
@@ -1 +1 @@
1
- {".":"0.14.0","wasm/kobako-core":"0.8.0","wasm/kobako-mruby":"0.8.0","wasm/kobako-io":"0.8.0","wasm/kobako-json":"0.8.0","wasm/kobako-regexp":"0.8.0","wasm/kobako-baker":"0.8.0","crates/kobako-codec":"0.8.0","crates/kobako-runtime":"0.8.0","crates/kobako-wasmtime":"0.8.0","crates/kobako":"0.8.0"}
1
+ {".":"0.15.0","wasm/kobako-core":"0.9.0","wasm/kobako-mruby":"0.9.0","wasm/kobako-io":"0.9.0","wasm/kobako-json":"0.9.0","wasm/kobako-regexp":"0.9.0","wasm/kobako-baker":"0.9.0","crates/kobako-codec":"0.9.0","crates/kobako-runtime":"0.9.0","crates/kobako-wasmtime":"0.9.0","crates/kobako":"0.9.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.15.0](https://github.com/elct9620/kobako/compare/v0.14.0...v0.15.0) (2026-07-11)
4
+
5
+
6
+ ### Features
7
+
8
+ * **claude:** cover rake files in the post-edit rubocop hook ([416cd42](https://github.com/elct9620/kobako/commit/416cd42009e7888fb0662e91719f4d7d61713cf8))
9
+ * **examples:** add plugin-rs, a Rust SDK narrative example ([8263096](https://github.com/elct9620/kobako/commit/8263096876900aa2c3ffeebbd24f7cca3571b893))
10
+ * **examples:** add wire-rs, a low-level host without the SDK ([d9edea8](https://github.com/elct9620/kobako/commit/d9edea8ed025e4b7ffe58be8c376c7fb0a65ac04))
11
+ * **sandbox:** flatten Service registration to path-valued bind ([0876006](https://github.com/elct9620/kobako/commit/0876006455544fd82eb7555ee80c149d98843719))
12
+ * **tasks:** add grouped test:tasks / test:bench runs ([1153c43](https://github.com/elct9620/kobako/commit/1153c43416363d7cc03b696eb2762b7281a68e93))
13
+ * **tasks:** compare the wire-symmetric peer inventories mechanically ([50d4fe5](https://github.com/elct9620/kobako/commit/50d4fe523bf85406931fa9611b6528c3de1a3f50))
14
+ * **tasks:** flag stale ledger entries in the surface and symmetry instruments ([d18054f](https://github.com/elct9620/kobako/commit/d18054f5daa6ce277154c288d5c3085133f441b4))
15
+ * **tasks:** hold the pub-surface crate map to the repo's crate roster ([4f65e79](https://github.com/elct9620/kobako/commit/4f65e79c3aa37ed1fc47a5002c8a567c3df8cc84))
16
+ * **tasks:** hold the tier roster to the repo and share it across instruments ([9a75586](https://github.com/elct9620/kobako/commit/9a755866236c134ea7601006211338eefae0a348))
17
+ * **tasks:** inventory exported macros in the pub-surface scan ([e4457ea](https://github.com/elct9620/kobako/commit/e4457eafecfa24eb5fc9aade086f2be9b0862818))
18
+ * **tasks:** measure the tooling tiers in stats and hotspots ([8def9ff](https://github.com/elct9620/kobako/commit/8def9ff96d36c2020e04d7e9bb0d9cd3fdad3e43))
19
+ * **tasks:** pin the tier roster to the repo from both sides ([3b7a154](https://github.com/elct9620/kobako/commit/3b7a154ebbfe8629392a81599df37d136a0eec99))
20
+ * **tasks:** rank churn x size x fan-in hotspots since the last release ([73b3bc8](https://github.com/elct9620/kobako/commit/73b3bc8fdee19f8c09a91ccf96348688ea3ccd1c))
21
+ * **tasks:** read the anchor citation profile back as an instrument ([47961cb](https://github.com/elct9620/kobako/commit/47961cb689d9c681f5c697af7e6bca52a4c7817f))
22
+ * **tasks:** report pub items no in-repo downstream consumes ([3eb0481](https://github.com/elct9620/kobako/commit/3eb048199a0e2e41509513750bec4fcbc6b7cc69))
23
+ * **tasks:** resolve the SPEC-local F/J/N families in the anchors gate ([33d3118](https://github.com/elct9620/kobako/commit/33d31181be3a800a4cc3d4eb32324d840473401f))
24
+ * **tasks:** scan pub statics in the pub-surface instrument ([72cff5e](https://github.com/elct9620/kobako/commit/72cff5ef369a73c353ecdd25c1d4688ec8dd2f82))
25
+ * **tasks:** score hotspots on impl-only lines for Rust files ([704b419](https://github.com/elct9620/kobako/commit/704b41971eac9e284b570bd0ac2fdd9af656bbca))
26
+ * **tasks:** seat anchors:coverage and wire:symmetry in the release gate ([ab035bb](https://github.com/elct9620/kobako/commit/ab035bb3c4e049e6319d33cdaf1d715d4e83eedb))
27
+ * **tasks:** widen the fan-in and ext-code scans to their whole tiers ([43d2bad](https://github.com/elct9620/kobako/commit/43d2badfea3c13cb062e67d5ded8410a339862d3))
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * **codec:** reject ext 0x02 anywhere in the Panic frame, not only details ([062e29d](https://github.com/elct9620/kobako/commit/062e29d6ee15264e1bd942502b751cfe7610acad))
33
+ * **codec:** reject the Fault envelope in Rust host payload positions ([bdf2ed7](https://github.com/elct9620/kobako/commit/bdf2ed78fde2798bdc15f4e969bda228cf482f4b))
34
+ * **codec:** reject the reserved Handle id 0 on the Rust wire tier ([5f7e482](https://github.com/elct9620/kobako/commit/5f7e4821680e553da355d5257b0619e4a1cdce72))
35
+ * **tasks:** fail the anchors gate when no SPEC ceiling parses ([8d185f5](https://github.com/elct9620/kobako/commit/8d185f59afc28b5334d934f6916fd64e458e9359))
36
+ * **tasks:** inventory every codec-bearing class per transport file ([e553ae1](https://github.com/elct9620/kobako/commit/e553ae1578245d9bfca63cf4e8429666857dbb92))
37
+ * **tasks:** keep tooling-suite fixture tokens out of the anchor scans ([58a78b9](https://github.com/elct9620/kobako/commit/58a78b990684d4ad87d4db18b21eb8bd1c98713a))
38
+ * **tasks:** mark unmeasured fan-in as '-' in the hotspot report ([a4959fe](https://github.com/elct9620/kobako/commit/a4959fe0c93546711dd16002e38d993e350bcb8f))
39
+ * **tasks:** read pub fn qualifiers in the pub-surface scan ([ac0e7d3](https://github.com/elct9620/kobako/commit/ac0e7d3030729ad981b6a8de504c5837daf2038d))
40
+ * **tasks:** read the codec-bearing class in the wire-symmetry inventory ([85ce937](https://github.com/elct9620/kobako/commit/85ce937fdb35953b26fee5fa4ee55f53988147ea))
41
+ * **tasks:** scan the whole tier in every wire-symmetry inventory ([c2bba5c](https://github.com/elct9620/kobako/commit/c2bba5c02ca20dca14d061a03f8f4581d0995a66))
42
+ * **tasks:** truncate the pub-surface scan only at the test module ([9c6dc05](https://github.com/elct9620/kobako/commit/9c6dc054bfa1fe79c3410fa794290a1ce5b85f4d))
43
+ * **test:** flunk instead of skip when CI lacks a built guest prerequisite ([f7818f6](https://github.com/elct9620/kobako/commit/f7818f68c8114e5f9b0012a4dd4b069695bc3e32))
44
+ * **transport:** refuse the Fault envelope outside its legal position (E-50) ([58c5069](https://github.com/elct9620/kobako/commit/58c50691d32e245f41c2f8041dfb7e92b05b80e9))
45
+
46
+
47
+ ### Performance Improvements
48
+
49
+ * **bench:** re-bless the anchor onto the wasmtime 46 round ([9225702](https://github.com/elct9620/kobako/commit/9225702106c0c4caf90425ac08c11452b6388b55))
50
+ * **dispatch:** skip the Handle walk when the request carried none ([cd63514](https://github.com/elct9620/kobako/commit/cd6351459c492084a401fd1b6a8e4293761de93f))
51
+ * **sandbox:** skip the Handle walk when the invocation result carried none ([d2c4947](https://github.com/elct9620/kobako/commit/d2c494782d316df3bbf5fc223a8beaa1f04f5718))
52
+ * **transport:** skip the Handle walk when the yield result carried none ([bca463b](https://github.com/elct9620/kobako/commit/bca463bce0b8913941b3e3ba11c31cb41b843545))
53
+
3
54
  ## [0.14.0](https://github.com/elct9620/kobako/compare/v0.13.0...v0.14.0) (2026-07-08)
4
55
 
5
56
 
data/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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 Ruby 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.
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
- result = sandbox.eval(<<~RUBY)
48
- 1 + 2
49
- RUBY
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.8"` | 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
- result # => 3
127
+ kobako_core::export_guest!(MyGuest);
52
128
  ```
53
129
 
54
- The script executes inside the Wasm guest. It cannot read your filesystem, open sockets, or touch your `ENV`.
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 Ruby object injected under `<Namespace>::<Member>` — the guest's only path to host resources. |
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
- Declare a Namespace, then `bind` any Ruby object as a Member; the guest reaches it as a `<Namespace>::<Member>` proxy and invokes its public methods through the Transport wire. See [`docs/behavior/registration.md`](docs/behavior/registration.md) B-07..B-12.
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.define(:Project).bind(:User, User.new(name: "alice"))
84
- sandbox.define(:KV) .bind(:Lookup, ->(key) { redis.get(key) })
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
- Names 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 `#define` raises `ArgumentError`.
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.define(:KV).bind(:Lookup, ...)
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 define / preload now
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 ~30 µ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).
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.define(:KV).bind(:Lookup, ->(key) { redis.get(key) })
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.define(:Seq).bind(:Map, ->(items, &blk) { items.map(&blk) })
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.define(:Factory).bind(:Make, ->(name) { Greeter.new(name) })
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 Member 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.
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.define(:Cfg).bind(:Settings, ThemeReader.new) # not: bind(:Settings, AppConfig)
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 `<Namespace>::<Member>` path, but a forged name only resolves to
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) | ~5 ms one-time |
346
- | Subsequent `Sandbox.new` (caches warm) | ~30 µs |
347
- | Warm `#eval("nil")` on a reused Sandbox | ~73 µs |
348
- | Warm `#run(:Entrypoint, ...)` dispatch | ~104 µs |
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 | ~8 µs each |
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 ~32 MB total. `ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput stays around 16k `#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.
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 through Namespaces / Members, opaque
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 Namespace declaration](docs/behavior/registration.md) | [lib/kobako/catalog/namespaces.rb](lib/kobako/catalog/namespaces.rb) | — |
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 `Namespace::Member` you never bound; or 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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.8.0...kobako-runtime-v0.9.0) (2026-07-11)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **kobako-runtime:** Synchronize kobako crates versions
9
+
3
10
  ## [0.8.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.7.0...kobako-runtime-v0.8.0) (2026-07-08)
4
11
 
5
12
 
@@ -26,7 +26,7 @@ map the neutral types onto their own language surface.
26
26
 
27
27
  ```toml
28
28
  [dependencies]
29
- kobako-runtime = "0.8.0" # x-release-please-version
29
+ kobako-runtime = "0.9.0" # x-release-please-version
30
30
  ```
31
31
 
32
32
  ## License
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.8.0...kobako-wasmtime-v0.9.0) (2026-07-11)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **kobako-wasmtime:** Synchronize kobako crates versions
9
+
3
10
  ## [0.8.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.7.0...kobako-wasmtime-v0.8.0) (2026-07-08)
4
11
 
5
12
 
@@ -24,7 +24,7 @@ SDK consumes the same surface.
24
24
 
25
25
  ```toml
26
26
  [dependencies]
27
- kobako-wasmtime = "0.8.0" # x-release-please-version
27
+ kobako-wasmtime = "0.9.0" # x-release-please-version
28
28
  ```
29
29
 
30
30
  ## License
data/data/kobako.wasm CHANGED
Binary file
Binary file
Binary file
Binary file
@@ -6,7 +6,7 @@ module Kobako
6
6
  module Catalog
7
7
  # Host-side mapping from opaque integer Handle IDs to Ruby objects.
8
8
  # The table is owned by +Kobako::Sandbox+ and injected
9
- # into the per-Sandbox +Kobako::Catalog::Namespaces+ so guest→host dispatch
9
+ # into the per-Sandbox +Kobako::Catalog::Services+ so guest→host dispatch
10
10
  # resolves Handle targets and arguments against the same table that
11
11
  # host→guest wire encoding allocates into.
12
12
  #
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "handles"
4
+ require_relative "../codec"
5
+ require_relative "../errors"
6
+
7
+ module Kobako
8
+ module Catalog
9
+ # Kobako::Catalog::Services — per-Sandbox registry of Service
10
+ # bindings keyed by their constant-path name. Holds the flat
11
+ # path→object table and the preamble emitted on Frame 1.
12
+ #
13
+ # Public API:
14
+ #
15
+ # services = Kobako::Catalog::Services.new
16
+ # services.bind("MyService::KV", kv_object) # => services (chainable)
17
+ # services.encode # => msgpack bytes for Frame 1
18
+ # services.lookup("MyService::KV") # => kv_object
19
+ #
20
+ # Per-dispatch routing is +Kobako::Transport::Dispatcher+'s
21
+ # responsibility — the Dispatcher receives this registry and the
22
+ # +Catalog::Handles+ as arguments from the +Runtime#on_dispatch+ Proc
23
+ # that +Kobako::Sandbox#initialize+ installs. The registry holds an
24
+ # injected +Catalog::Handles+ reference so dispatch target resolution
25
+ # and host→guest auto-wrap share the same Sandbox-owned allocator.
26
+ class Services
27
+ # Ruby constant-name pattern each +::+-separated bind-path segment
28
+ # must match.
29
+ NAME_PATTERN = /\A[A-Z]\w*\z/
30
+
31
+ # Build a fresh registry. +handler+ is an internal seam that injects
32
+ # a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
33
+ # is pinned near +MAX_ID+ to exercise the cap-exhaustion path
34
+ # without 2³¹ allocations. Production callers leave it at the default.
35
+ def initialize(handler: Catalog::Handles.new)
36
+ @bindings = {} # : Hash[String, untyped]
37
+ @handler = handler
38
+ @sealed = false
39
+ @encoded = nil # : String?
40
+ end
41
+
42
+ # Bind +object+ as the Service reachable at +path+ — a +Symbol+ or
43
+ # +String+ of one or more +::+-separated constant-form segments
44
+ # (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
45
+ # chaining. Raises +ArgumentError+ when a segment is malformed, when
46
+ # +path+ collides with an existing binding (a name is a bound Service
47
+ # or a grouping prefix, never both), or when the owning Sandbox has
48
+ # been sealed by its first invocation.
49
+ def bind(path, object)
50
+ raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
51
+
52
+ path_str = validate_path!(path)
53
+ raise ArgumentError, "Service path #{path_str} conflicts with an existing binding" if collision?(path_str)
54
+
55
+ @bindings[path_str] = object
56
+ self
57
+ end
58
+
59
+ # Resolve a +target+ constant path to the bound Service. Raises
60
+ # +KeyError+ when no Service is bound at +target+.
61
+ def lookup(target)
62
+ target_str = target.to_s
63
+ raise KeyError, "no service bound at #{target_str.inspect}" unless @bindings.key?(target_str)
64
+
65
+ @bindings[target_str]
66
+ end
67
+
68
+ # Encode the preamble as msgpack bytes for stdin Frame 1 delivery —
69
+ # a flat array of the bound constant paths, in bind order:
70
+ # +["MyService::KV", "File"]+. Routes through Kobako::Codec::Encoder
71
+ # like every other host-side wire encode; the preamble carries only
72
+ # Strings, so none of the kobako ext types fire. Returns a binary
73
+ # +String+ of msgpack bytes.
74
+ #
75
+ # Once sealed, the bytes are computed once and reused for every
76
+ # subsequent invocation: sealing freezes Service registration at the
77
+ # first invocation, so a bind reaching the registry after the seal
78
+ # raises +ArgumentError+ and never alters Frame 1.
79
+ def encode
80
+ return @encoded if @encoded
81
+
82
+ bytes = Codec::Encoder.encode(@bindings.keys).freeze
83
+ @encoded = bytes if @sealed
84
+ bytes
85
+ end
86
+
87
+ # Mark the registry as sealed. Called by +Sandbox+ on the first
88
+ # invocation; afterwards #bind raises ArgumentError. Idempotent;
89
+ # returns +self+.
90
+ def seal!
91
+ @sealed = true
92
+ self
93
+ end
94
+
95
+ # Returns +true+ when #seal! has been called, +false+ otherwise.
96
+ def sealed?
97
+ @sealed
98
+ end
99
+
100
+ private
101
+
102
+ def validate_path!(path)
103
+ path_str = path.to_s
104
+ segments = path_str.split("::", -1)
105
+ return path_str if !segments.empty? && segments.all? { |seg| NAME_PATTERN.match?(seg) }
106
+
107
+ raise ArgumentError,
108
+ "bind path must be constant-form segments joined by '::' (got #{path.inspect})"
109
+ end
110
+
111
+ # A path collides when it equals, is a prefix of, or extends an
112
+ # existing binding on the +::+ segment boundary — the guardrail that
113
+ # keeps a name from being both a bound Service and a grouping prefix.
114
+ def collision?(path)
115
+ @bindings.each_key.any? do |existing|
116
+ existing == path ||
117
+ existing.start_with?("#{path}::") ||
118
+ path.start_with?("#{existing}::")
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -44,7 +44,7 @@ module Kobako
44
44
  # The bytes are memoized — the table is replayed verbatim on every
45
45
  # invocation after sealing, so Frame 3 never changes between
46
46
  # encodes; #register drops the memo while the table is still open.
47
- # Unlike +Catalog::Namespaces#encode+, which gates its memo on the
47
+ # Unlike +Catalog::Services#encode+, which gates its memo on the
48
48
  # seal, this one can fill eagerly and invalidate in +#register+
49
49
  # because every mutation funnels through that single method — there is
50
50
  # no out-of-sight child object to change the result behind its back.
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "catalog/handles"
4
- require_relative "catalog/namespaces"
4
+ require_relative "catalog/services"
5
5
  require_relative "catalog/snippets"
6
6
 
7
7
  module Kobako
8
8
  # Kobako::Catalog — Sandbox-level configuration and per-invocation
9
9
  # allocation tables. Houses the three host-side registries the Sandbox
10
- # owns: +Catalog::Namespaces+ (Namespace / Member registry),
10
+ # owns: +Catalog::Services+ (path→Service binding registry),
11
11
  # +Catalog::Snippets+ (preloaded source / bytecode entries), and
12
12
  # +Catalog::Handles+ (per-invocation Handle ID allocator).
13
13
  #
@@ -3,7 +3,7 @@
3
3
  require "msgpack"
4
4
 
5
5
  require_relative "error"
6
- require_relative "factory"
6
+ require_relative "ext_types"
7
7
  require_relative "utils"
8
8
 
9
9
  module Kobako
@@ -31,7 +31,7 @@ module Kobako
31
31
  # surfaces as InvalidType without a separate Utils.with_boundary
32
32
  # wrapper at the call site.
33
33
  def self.decode(bytes)
34
- value = Factory.load(bytes.b)
34
+ value = FACTORY.load(bytes.b)
35
35
  validate_utf8!(value)
36
36
  block_given? ? yield(value) : value
37
37
  # msgpack gem raises the format/type errors below; +ArgumentError+
@@ -54,7 +54,7 @@ module Kobako
54
54
  # str family but does not validate the bytes; +bin+ family decodes
55
55
  # to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
56
56
  # str-typed leaf via Utils.assert_utf8!. Kobako::Fault
57
- # payloads are validated transitively: +Factory.unpack_fault+
57
+ # payloads are validated transitively: +ExtTypes#unpack_fault+
58
58
  # feeds the inner ext-0x02 bytes back through this Decoder, so their
59
59
  # +str+ fields are already covered by the time control returns here.
60
60
  class << self