kobako 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +377 -0
  4. data/Cargo.lock +181 -154
  5. data/README.md +99 -459
  6. data/ROADMAP.md +7 -1
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +354 -0
  9. data/crates/kobako-runtime/Cargo.toml +38 -7
  10. data/crates/kobako-runtime/README.md +17 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +9 -1
  12. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  13. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  14. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  15. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  16. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  17. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  18. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  19. data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
  20. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  21. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  29. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
  30. data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
  31. data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
  32. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  33. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  37. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  38. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  39. data/crates/kobako-runtime/src/filesystem.rs +26 -0
  40. data/crates/kobako-runtime/src/grant.rs +20 -0
  41. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  42. data/crates/kobako-runtime/src/lib.rs +16 -1
  43. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  44. data/crates/kobako-runtime/src/locking.rs +218 -0
  45. data/crates/kobako-runtime/src/mount.rs +105 -0
  46. data/crates/kobako-runtime/src/profile.rs +140 -4
  47. data/crates/kobako-runtime/src/runtime.rs +12 -0
  48. data/crates/kobako-runtime/src/sched.rs +210 -0
  49. data/crates/kobako-runtime/src/security.rs +696 -0
  50. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  51. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  52. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  53. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  54. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  55. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  56. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  57. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  58. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  59. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  60. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  61. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  62. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  63. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  64. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  65. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  66. data/crates/kobako-runtime/src/vfs.rs +60 -0
  67. data/crates/kobako-runtime/src/yielder.rs +11 -6
  68. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  70. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  71. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  72. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  73. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  74. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  75. data/crates/kobako-transport/CHANGELOG.md +23 -0
  76. data/crates/kobako-transport/Cargo.toml +3 -3
  77. data/crates/kobako-transport/README.md +6 -5
  78. data/crates/kobako-transport/src/abi.rs +1 -1
  79. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  80. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  81. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  83. data/crates/kobako-transport/src/envelope/fault.rs +4 -3
  84. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  85. data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
  86. data/crates/kobako-transport/src/envelope/reply.rs +34 -16
  87. data/crates/kobako-transport/src/envelope/request.rs +19 -9
  88. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  89. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  90. data/crates/kobako-transport/src/envelope.rs +2 -2
  91. data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
  92. data/crates/kobako-wasmtime/Cargo.toml +25 -19
  93. data/crates/kobako-wasmtime/README.md +13 -7
  94. data/crates/kobako-wasmtime/src/abi.rs +75 -49
  95. data/crates/kobako-wasmtime/src/ambient.rs +64 -6
  96. data/crates/kobako-wasmtime/src/cache.rs +188 -77
  97. data/crates/kobako-wasmtime/src/config.rs +77 -5
  98. data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
  99. data/crates/kobako-wasmtime/src/driver.rs +70 -135
  100. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  101. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  102. data/crates/kobako-wasmtime/src/imports.rs +52 -0
  103. data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
  104. data/crates/kobako-wasmtime/src/invocation.rs +43 -74
  105. data/crates/kobako-wasmtime/src/lib.rs +5 -4
  106. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  107. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
  108. data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
  109. data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
  110. data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
  111. data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
  112. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  113. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
  114. data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
  115. data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
  116. data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
  117. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  118. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  119. data/crates/kobako-wasmtime/src/preview1.rs +512 -0
  120. data/crates/kobako-wasmtime/src/trap.rs +45 -25
  121. data/crates/kobako-wasmtime/src/wasi.rs +333 -0
  122. data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
  123. data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
  124. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  125. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
  126. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
  127. data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
  128. data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
  129. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
  130. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  131. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  132. data/data/kobako.wasm +0 -0
  133. data/ext/kobako/Cargo.toml +4 -2
  134. data/ext/kobako/src/runtime/bridge.rs +154 -81
  135. data/ext/kobako/src/runtime/errors.rs +1 -1
  136. data/ext/kobako/src/runtime/escape.rs +67 -0
  137. data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
  138. data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
  139. data/ext/kobako/src/runtime/filesystem.rs +208 -0
  140. data/ext/kobako/src/runtime/gvl.rs +49 -8
  141. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  142. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  143. data/ext/kobako/src/runtime.rs +125 -199
  144. data/lib/kobako/catalog/extensions.rb +5 -5
  145. data/lib/kobako/catalog/flags.rb +2 -14
  146. data/lib/kobako/catalog/grants.rb +30 -0
  147. data/lib/kobako/catalog/handles.rb +6 -6
  148. data/lib/kobako/catalog/mount.rb +19 -0
  149. data/lib/kobako/catalog/services.rb +13 -2
  150. data/lib/kobako/catalog/snippets.rb +11 -15
  151. data/lib/kobako/catalog.rb +11 -0
  152. data/lib/kobako/codec/decoder.rb +10 -9
  153. data/lib/kobako/codec/ext_types.rb +4 -6
  154. data/lib/kobako/codec/handle_walk.rb +4 -6
  155. data/lib/kobako/codec/nesting.rb +7 -7
  156. data/lib/kobako/codec/state.rb +19 -3
  157. data/lib/kobako/codec/utils.rb +1 -1
  158. data/lib/kobako/codec/value_budget.rb +86 -0
  159. data/lib/kobako/codec.rb +7 -0
  160. data/lib/kobako/context.rb +40 -13
  161. data/lib/kobako/errors.rb +15 -14
  162. data/lib/kobako/filesystem.rb +69 -0
  163. data/lib/kobako/payload/arguments.rb +2 -2
  164. data/lib/kobako/pool.rb +6 -6
  165. data/lib/kobako/sandbox.rb +55 -27
  166. data/lib/kobako/sandbox_options.rb +39 -13
  167. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
  168. data/lib/kobako/snippet/source.rb +1 -1
  169. data/lib/kobako/snippet.rb +1 -1
  170. data/lib/kobako/transport/block_failure.rb +35 -0
  171. data/lib/kobako/transport/dispatcher.rb +33 -29
  172. data/lib/kobako/transport/exposure.rb +7 -9
  173. data/lib/kobako/transport/reflection.rb +79 -23
  174. data/lib/kobako/transport/run.rb +5 -5
  175. data/lib/kobako/transport/yielder.rb +9 -31
  176. data/lib/kobako/transport.rb +1 -0
  177. data/lib/kobako/version.rb +1 -1
  178. data/lib/kobako.rb +35 -0
  179. data/release-please-config.json +1 -0
  180. data/sig/kobako/catalog/extensions.rbs +3 -3
  181. data/sig/kobako/catalog/flags.rbs +0 -4
  182. data/sig/kobako/catalog/grants.rbs +13 -0
  183. data/sig/kobako/catalog/handles.rbs +3 -3
  184. data/sig/kobako/catalog/mount.rbs +24 -0
  185. data/sig/kobako/catalog/services.rbs +5 -1
  186. data/sig/kobako/catalog/snippets.rbs +4 -4
  187. data/sig/kobako/catalog.rbs +1 -0
  188. data/sig/kobako/codec/decoder.rbs +1 -1
  189. data/sig/kobako/codec/handle_walk.rbs +1 -1
  190. data/sig/kobako/codec/nesting.rbs +2 -2
  191. data/sig/kobako/codec/state.rbs +7 -1
  192. data/sig/kobako/codec/utils.rbs +1 -1
  193. data/sig/kobako/codec/value_budget.rbs +23 -0
  194. data/sig/kobako/codec.rbs +3 -2
  195. data/sig/kobako/context.rbs +14 -6
  196. data/sig/kobako/errors.rbs +4 -4
  197. data/sig/kobako/filesystem.rbs +93 -0
  198. data/sig/kobako/guest_yielder.rbs +9 -0
  199. data/sig/kobako/payload/arguments.rbs +1 -1
  200. data/sig/kobako/pool.rbs +3 -3
  201. data/sig/kobako/runtime.rbs +21 -49
  202. data/sig/kobako/sandbox.rbs +9 -5
  203. data/sig/kobako/sandbox_options.rbs +25 -3
  204. data/sig/kobako/snapshot.rbs +20 -0
  205. data/sig/kobako/snippet/bytecode.rbs +12 -0
  206. data/sig/kobako/transport/block_failure.rbs +13 -0
  207. data/sig/kobako/transport/dispatcher.rbs +4 -4
  208. data/sig/kobako/transport/exposure.rbs +2 -2
  209. data/sig/kobako/transport/reflection.rbs +14 -1
  210. data/sig/kobako/transport/run.rbs +3 -3
  211. data/sig/kobako/transport/yielder.rbs +3 -7
  212. data/sig/kobako/transport.rbs +3 -3
  213. metadata +116 -15
  214. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  215. data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
  216. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
  217. data/crates/kobako-wasmtime/src/io.rs +0 -286
  218. data/sig/kobako/snippet/binary.rbs +0 -12
data/README.md CHANGED
@@ -2,573 +2,213 @@
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. 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
-
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.
5
+ Kobako is a Ruby gem that runs untrusted mruby scripts in an in-process Wasm
6
+ sandbox: LLM-generated code, user formulas, student submissions, plugins. A
7
+ script reaches no host memory, file, network, or credential you did not hand it.
8
+ A Rust SDK offers the same sandbox to hosts in other languages.
8
9
 
9
10
  ```
10
- Host process Wasm guest
11
- ┌──────────────────────┐ ┌──────────────────────┐
12
- │ Kobako::Sandbox │ ─eval─▶ │ mruby interpreter │
13
- │ │ ─run──▶ │ │
14
- │ Services │ ◀─call─ │ KV::Lookup.call(k) │
15
- │ KV::Lookup │ ─resp─▶ │ │
16
- │ │ │ │
17
- │ Execution │ ◀─pipe─ │ puts / warn │
18
- │ value │ ◀─last─ │ last expression │
19
- │ stdout / stderr │ │ │
20
- │ usage │ │ │
21
- └──────────────────────┘ └──────────────────────┘
22
- trusted untrusted
11
+ Host process Wasm guest
12
+ +---------------------+ +---------------------+
13
+ | Kobako::Sandbox |--eval-->| mruby interpreter |
14
+ | Services |<--call--| KV::Lookup.call(k) |
15
+ | Execution |<-result-| last expression |
16
+ +---------------------+ +---------------------+
17
+ trusted untrusted
23
18
  ```
24
19
 
25
20
  ## Requirements
26
21
 
22
+ The gem ships its Guest Binary and, on most platforms, a prebuilt native
23
+ extension.
24
+
27
25
  | What | Which versions | When you need it |
28
26
  |---|---|---|
29
- | Ruby | ≥ 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) |
33
-
34
- The precompiled `kobako.wasm` Guest Binary ships inside the gem, so end users do **not** need a WASI toolchain.
27
+ | Ruby | >= 3.3.0 | always |
28
+ | Platform | Linux, macOS, or Windows | always; Windows gaps are fixed last |
29
+ | Rust / Cargo | current stable | only where no prebuilt gem matches your platform |
30
+ | WASI toolchain | vendored by the build | only when building from a source checkout |
35
31
 
36
32
  ## Installation
37
33
 
38
- Install the gem; it ships its Guest Binary prebuilt.
34
+ Add the gem to your bundle.
39
35
 
40
36
  ```bash
41
37
  bundle add kobako
42
- # or
43
- gem install kobako
44
38
  ```
45
39
 
46
40
  ## Quick Start
47
41
 
48
- 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.
42
+ A Sandbox runs guest code and returns the record of that run.
49
43
 
50
44
  ```ruby
51
45
  require "kobako"
52
46
 
53
47
  sandbox = Kobako::Sandbox.new
54
- sandbox.eval("1 + 2").value # => 3
48
+ sandbox.eval("1 + 2").value # => 3
55
49
  ```
56
50
 
57
51
  ```rust
58
52
  use kobako::{Options, Sandbox};
59
53
 
60
- // Options::default() is secure by default: the same caps as Ruby, hermetic isolation.
61
54
  let sandbox = Sandbox::new("kobako.wasm", Options::default())?;
62
- sandbox.eval("1 + 2")?.value()?; // => Value::Int(3)
63
- ```
64
-
65
- Each invocation returns the record of that one run: `Kobako::Execution` in Ruby, `Execution` in Rust. It carries the guest value alongside the run's output and resource usage. Nothing a run observes is stored on the Sandbox, so the same Sandbox serves concurrent invocations without them seeing each other.
66
-
67
- The gem bundles its Guest Binary; a Rust host loads one explicitly — see [Frontends & Guest Binaries](#frontends--guest-binaries) for the packages and the Guest Binary variants.
68
-
69
- ## Frontends & Guest Binaries
70
-
71
- Embedding kobako is two independent choices: the **host frontend** you build against, and the **Guest Binary** it runs.
72
-
73
- ```text
74
- host frontend Guest Binary
75
- Ruby gem ──┐ ┌── kobako.wasm (pure)
76
- Rust SDK ──┼──┼── kobako+regexp.wasm
77
- your own ──┘ └── kobako+json.wasm · your own
78
- ```
79
-
80
- They compose freely, since any frontend loads any Guest Binary. A Ruby host can run a JSON-enabled guest, and a Rust host can run the pure default. How far down those choices go — and which ones a given starting point quietly makes for you — is laid out in [`docs/guides/assembly-levels.md`](docs/guides/assembly-levels.md).
81
-
82
- ### Host frontends
83
-
84
- The host embeds the sandbox and owns the wire codec. Choose by your host language and how much of the contract you want pre-assembled.
85
-
86
- | Frontend | Package | Add it | Best for |
87
- |----------|---------|--------|----------|
88
- | Ruby gem | `kobako` (RubyGems) | `gem install kobako` | A Ruby host — Services, Handles, snippets, and pooling out of the box |
89
- | Rust SDK | `kobako` (crates.io) | `cargo add kobako` | A Rust host — the same behavior contract behind an idiomatic Rust API |
90
- | Low-level crates | `kobako-wasmtime` + `kobako-runtime` + `kobako-transport` + `kobako-codec` | `cargo add` each | A custom host, or driving the wire directly in another language |
91
-
92
- The Rust crates are documented on [crates.io](https://crates.io/crates/kobako); the Ruby gem is this README. Three runnable Rust hosts show the choice. [`plugin-rs`](examples/plugin-rs) builds on the SDK, and [`wire-rs`](examples/wire-rs) assembles a host by hand on the low-level crates. [`fixed-schema-rs`](examples/fixed-schema-rs) replaces the payload schema both of those keep, with a guest and a host that agree on protobuf.
93
-
94
- ### Pre-built Guest Binaries
95
-
96
- The gem bundles the pure `kobako.wasm`. Regexp and JSON are opt-in capabilities compiled into separate variants, each attached to every [GitHub Release](https://github.com/elct9620/kobako/releases). Download a variant and point your host at it — `Sandbox.new(wasm_path:)` in Ruby, `Sandbox::new(path, ...)` in Rust. [`docs/guides/variants.md`](docs/guides/variants.md) lists every variant and what it adds.
97
-
98
- ```ruby
99
- sandbox = Kobako::Sandbox.new(wasm_path: "kobako+full.wasm")
100
- sandbox.eval('JSON.generate({ n: "42".to_i })').value # => "{\"n\":42}"
55
+ sandbox.eval("1 + 2")?.value()?; // => Value::Int(3)
101
56
  ```
102
57
 
103
- ### Custom Guest Binaries
58
+ These terms name what the rest of this README builds on.
104
59
 
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.
106
-
107
- | To decide | Read |
60
+ | Term | Meaning |
108
61
  |---|---|
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) |
112
-
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.
116
-
117
- ```ruby
118
- sandbox.flag(:strict_mode) # critical: refused where unrecognised
119
- sandbox.flag(:compile_cache, "on", advisory: true) # skipped where unrecognised
120
- ```
62
+ | Sandbox | the unit you configure once and invoke many times |
63
+ | Invocation | one run of guest code, by `#eval` or `#run` |
64
+ | Execution | the frozen record an Invocation leaves |
65
+ | Service | a host object the guest reaches by name |
66
+ | Mount | a Filesystem placed at a guest path |
121
67
 
122
- ## Glossary
68
+ ## Features
123
69
 
124
- These terms name the concepts the rest of this README builds on.
70
+ Each feature below shows its smallest use; the [guides] hold the rest.
125
71
 
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.
138
-
139
- ## Usage
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) |
72
+ [guides]: https://github.com/elct9620/kobako/tree/main/docs/guides
150
73
 
151
74
  ### Services
152
75
 
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.
76
+ `bind` makes a host object reachable from the guest at a constant name.
154
77
 
155
78
  ```ruby
156
- class User
157
- attr_reader :name
158
-
159
- def initialize(name:)
160
- @name = name
161
- end
162
- end
163
-
164
- sandbox.bind("Project::User", User.new(name: "alice"))
165
- sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
166
-
167
- sandbox.eval(<<~RUBY)
168
- Project::User.name # => "alice"
169
- KV::Lookup.call("user_42") # => "..."
170
- RUBY
79
+ sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
80
+ sandbox.eval('KV::Lookup.call("user_42")').value
171
81
  ```
172
82
 
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`.
83
+ A Service can also be filled per Invocation, take a guest block, and hand back
84
+ host objects as opaque Handles.
174
85
 
175
- ### Per-Invocation Bindings
86
+ ### Mounts
176
87
 
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.
88
+ `mount` gives the guest's `File` and `Dir` a Filesystem at one path, read-only by default.
178
89
 
179
90
  ```ruby
180
- sandbox.bind("Req::Current") # declared, unfilled — stands for Kobako::Unresolved
181
-
182
- sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
91
+ sandbox.mount("/config", Kobako::Filesystem::Tmpfs.new("app.yml" => yaml))
92
+ sandbox.eval('File.read("/config/app.yml")').value
183
93
  ```
184
94
 
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.
95
+ | Kind | Holds |
96
+ |---|---|
97
+ | `Tmpfs` | files in memory |
98
+ | `Bind` | one host directory |
99
+ | `Overlay` | read-only Layers under a Tmpfs |
186
100
 
187
- ### Output Capture
101
+ ### Execution Records
188
102
 
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?`.
103
+ An Execution carries the value, the output, and the usage. A failed run raises
104
+ one of three classes, each carrying its Execution.
190
105
 
191
106
  ```ruby
192
- execution = sandbox.eval(<<~RUBY)
193
- puts "hello"
194
- warn "be careful"
195
- 42
196
- RUBY
197
-
198
- execution.value # => 42
199
- execution.stdout # => "hello\n"
200
- execution.stderr # => "be careful\n"
201
- ```
202
-
203
- ### Error Handling
107
+ execution = sandbox.eval('puts "hi"; 42')
108
+ execution.stdout # => "hi\n"
204
109
 
205
- Every invocation either returns an Execution or raises exactly one of three classes, so you can route faults without inspecting messages. The full taxonomy lives in [`lib/kobako/errors.rb`](lib/kobako/errors.rb).
206
-
207
- ```ruby
208
110
  begin
209
111
  sandbox.eval(script)
210
- rescue Kobako::TrapError
211
- # Wasm engine fault or cap exhaustion. The invocation is over.
212
- rescue Kobako::ServiceError
213
- # A host Service call failed and the script did not rescue it.
214
- rescue Kobako::SandboxError => e
215
- # The script raised, failed to compile, or returned an unrepresentable value,
216
- # or the Guest Binary could not start the run.
217
- logger.warn(e.execution.stderr) # the failed run's own captures and usage
112
+ rescue Kobako::TrapError, Kobako::ServiceError, Kobako::SandboxError => e
113
+ e.execution&.stderr
218
114
  end
219
115
  ```
220
116
 
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
-
223
- | Class | Parent | Trigger |
224
- |---------------------------------|----------------|------------------------------------------------------|
225
- | `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
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 |
229
- | `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
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 |
232
- | `Kobako::UndefinedEntrypointError` | `SandboxError` | `#run` named a constant no snippet defined; carries `#name` and `#available` |
233
-
234
- `SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` fields when the guest produced a panic envelope.
235
-
236
117
  ### Resource Limits
237
118
 
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.
119
+ Each Invocation runs under caps, and reaching one ends that run alone.
239
120
 
240
121
  ```ruby
241
- sandbox = Kobako::Sandbox.new(
242
- timeout: 5.0, # seconds, default 60.0
243
- memory_limit: 10 * 1024 * 1024, # bytes, default 1 MiB
244
- stdout_limit: 64 * 1024, # bytes, default 1 MiB
245
- stderr_limit: 64 * 1024
246
- )
122
+ Kobako::Sandbox.new(timeout: 5.0, memory_limit: 10 * 1024 * 1024)
247
123
  ```
248
124
 
249
- | Cap | Raises | Default |
250
- |----------------|----------------------------|---------|
251
- | `timeout` | `Kobako::TimeoutError` | 60.0 s |
252
- | `memory_limit` | `Kobako::MemoryLimitError` | 1 MiB |
253
- | `stdout_limit` | output clipped (no raise) | 1 MiB |
254
- | `stderr_limit` | output clipped (no raise) | 1 MiB |
255
-
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.
257
-
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.
259
-
260
- ### Concurrency
261
-
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 |
268
-
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.
272
-
273
- ```ruby
274
- sandbox = Kobako::Sandbox.new(gvl: :release)
275
- ```
276
-
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.
278
-
279
- ### Invocation Lifecycle
280
-
281
- One Sandbox serves many invocations. Service bindings and preloaded snippets persist across calls; everything a run produces — Handles, captured output, memory delta — belongs to that run alone.
282
-
283
- ```
284
- ───────────── setup phase (mutable) ─────────────
285
-
286
- sandbox = Kobako::Sandbox.new
287
- sandbox.bind("KV::Lookup", ...)
288
- sandbox.bind("Req::Current") # fillable: object comes later
289
- sandbox.preload(code: ..., name: :Adder)
290
- sandbox.preload(code: ..., name: :Greeter)
291
-
292
- │
293
- ▼
294
-
295
- ═════════════════ seal point ═════════════════
296
- First #eval or #run freezes the Service registry
297
- and snippet table. Further bind / preload now
298
- raise ArgumentError.
299
-
300
- │
301
- ▼
302
-
303
- ──────────────── invocation N ───────────────────
304
-
305
- 1. resolve this run's bindings, before the guest starts:
306
- ctx.bind("Req::Current", ...) fills the fillable
307
- provider: backends yield this run's object
308
-
309
- 2. start from the canonical boot state
310
- (mruby pre-initialized into the artifact at build time)
311
-
312
- 3. replay snippets (in insertion order):
313
- :Adder → defines Adder
314
- :Greeter → defines Greeter
315
-
316
- 4. dispatch: eval(source) or run(:Target, *args, **kwargs)
317
-
318
- 5. return the run's Execution to host
319
-
320
- 6. discard the instance; per-invocation state ends with it:
321
- · Handles invalidated
322
- · captures frozen into the Execution
323
- · memory delta zeroed
324
-
325
- Services + snippets persist; invocation N+1 repeats.
326
- ```
327
-
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.
329
-
330
- ### Pooling
331
-
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.
333
-
334
- ```ruby
335
- pool = Kobako::Pool.new(slots: 4) do |sandbox|
336
- sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
337
- end
338
-
339
- pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))).value }
340
- ```
341
-
342
- #### Options and lifetime
343
-
344
- | Option | Meaning | Default |
345
- |--------|---------|---------|
346
- | `slots:` | Upper bound on constructed Sandboxes | required |
347
- | `checkout_timeout:` | Seconds `#with` waits for a free Sandbox; `nil` waits indefinitely | 5.0 |
348
-
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 |
125
+ | Cap | Default |
354
126
  |---|---|
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.
358
-
359
- ### Service Blocks
360
-
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.
362
-
363
- ```ruby
364
- sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
127
+ | `timeout` | 60 s |
128
+ | `memory_limit` | 4 MiB |
129
+ | `stdout_limit`, `stderr_limit` | 1 MiB |
130
+ | `open_file_limit` | 1024 |
365
131
 
366
- sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }').value
367
- # => [2, 4, 6]
368
- ```
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
-
378
- ### Handle Management
132
+ ### Concurrency
379
133
 
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.
134
+ A Sandbox keeps nothing between runs, so Threads may share one. `gvl:
135
+ :release` runs guest code in parallel, and `Kobako::Pool` keeps warm Sandboxes.
381
136
 
382
137
  ```ruby
383
- class Greeter
384
- def initialize(name) = @name = name
385
- def greet = "hi, #{@name}"
386
- end
387
-
388
- sandbox.bind("Factory::Make", ->(name) { Greeter.new(name) })
389
-
390
- sandbox.eval('Factory::Make.call("Bob").greet').value # => "hi, Bob" (Handle round-trip inside guest)
391
- sandbox.eval('Factory::Make.call("Bob")').value # => #<Greeter @name="Bob"> (Handle restoration)
138
+ pool = Kobako::Pool.new(slots: 4) { |sandbox| sandbox.bind("KV::Lookup", lookup) }
139
+ pool.with { |sandbox| sandbox.eval(script).value }
392
140
  ```
393
141
 
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.
142
+ ### Snippets
395
143
 
396
- #### Per-dispatch Handles
397
-
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:
399
-
400
- ```
401
- guest chain host (Catalog::Handles, one invocation)
402
- ─────────── ─────────────────────────────────────────
403
- User.where(active: true) ─call──▶ Relation #1 (fresh clone) bound ▶ Handle 1
404
- ◀─Handle 1
405
- .order(:created_at) ─call──▶ Relation #2 (fresh clone) bound ▶ Handle 2
406
- ◀─Handle 2
407
- .limit(10) ─call──▶ Relation #3 (fresh clone) bound ▶ Handle 3
408
- ◀─Handle 3
409
-
410
- 3 hops ─▶ 3 dispatches ─▶ 3 distinct relations ─▶ 3 Handles
411
- all stay live until the invocation ends, then reset together
412
- ```
413
-
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.
415
-
416
- ### Snippets & Entrypoints
417
-
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.
144
+ `preload` replays guest code into every Invocation, and `run` calls what it
145
+ defined. An Extension pairs such code with a host backend.
419
146
 
420
147
  ```ruby
421
- sandbox = Kobako::Sandbox.new
422
- sandbox.preload(code: "Adder = ->(a, b) { a + b }", name: :Adder)
423
- sandbox.preload(code: 'Greeter = ->(opts) { "hello, #{opts[:name]}" }', name: :Greeter)
424
-
425
- sandbox.run(:Adder, 2, 3).value # => 5
426
- sandbox.run(:Greeter, name: "world").value # => "hello, world"
148
+ sandbox.preload(code: "Adder = ->(a, b) { a + b }", name: :Adder)
149
+ sandbox.run(:Adder, 2, 3).value # => 5
427
150
  ```
428
151
 
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.
430
-
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.
432
-
433
- ```
434
- per-invocation replay (every #eval / #run, snippets in insertion order):
435
-
436
- canonical boot state
437
- │
438
- ├──▶ replay :Adder (defines Adder)
439
- │
440
- ├──▶ replay :Greeter (defines Greeter)
441
- │
442
- └──▶ eval(source) -or- run(:Target, *args, **kwargs)
443
- │
444
- ▼
445
- return the Execution, then discard the instance
446
- ```
447
-
448
- #### Payload Forms
449
-
450
- `#preload` accepts two payload forms:
451
-
452
- | Form | Signature | Snippet name source | Validation timing |
453
- |----------|--------------------------------------|---------------------------------------|----------------------------------------------------------------------------|
454
- | Source | `preload(code: "...", name: :Const)` | The `name:` keyword | First invocation's replay; compile errors raise `Kobako::SandboxError` |
455
- | Bytecode | `preload(binary: bytes)` | Read from the bytecode's `debug_info` | First invocation's replay; structural failure raises `Kobako::BytecodeError` |
456
-
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.
152
+ ### Guest Binaries
458
153
 
459
- ### Extensions
460
-
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)).
154
+ The gem bundles the pure `kobako.wasm`. Each [GitHub Release] adds variants
155
+ with Regexp or JSON, and you can build your own in Rust.
462
156
 
463
157
  ```ruby
464
- FILE = <<~'MRUBY'
465
- class File
466
- extend Kobako::Proxy
467
- def self.basename(path) = path.split("/").last || "" # pure: runs in-guest
468
- # read / write are undefined here, so they dispatch to the backend
469
- end
470
- MRUBY
471
-
472
- sandbox.install(
473
- Kobako::Extension.new(
474
- name: :File,
475
- source: FILE,
476
- backend: Kobako::Extension::Backend.new(
477
- path: "File",
478
- provider: -> { OverlayFileSystem.new(root) } # invoked once per invocation
479
- )
480
- )
481
- )
482
-
483
- sandbox.eval('File.read("sample.txt")') # dispatches to the backend's #read
158
+ sandbox = Kobako::Sandbox.new(wasm_path: "kobako+full.wasm")
159
+ sandbox.eval('JSON.generate({ n: 42 })').value # => "{\"n\":42}"
484
160
  ```
485
161
 
486
- A backend declares the bound object's lifetime by keyword, never by inference — so a static object that happens to be callable stays unambiguous.
487
-
488
- | Keyword | Lifetime |
489
- |-------------|-----------------------------------------------------------------------------|
490
- | `object:` | One object, shared by every invocation |
491
- | `provider:` | A no-argument callable invoked per invocation |
492
- | neither | A fillable, standing for `Kobako::Unresolved` until `ctx.bind` supplies the run's object |
493
-
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.
162
+ [GitHub Release]: https://github.com/elct9620/kobako/releases
495
163
 
496
164
  ## Security
497
165
 
498
- kobako isolates the guest, but **what it may reach is whatever you `bind`** — and `bind`
499
- exposes every public method the object's own class defines. Inherited, mixed-in, and built-in
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.
166
+ kobako isolates the guest, but it reaches whatever you `bind` or `mount`. Bind
167
+ an object built for the task, since every public method its class defines is
168
+ exposed.
502
169
 
503
170
  ```ruby
504
- class ThemeReader # only #color is reachable; AppConfig.secret_key is not
171
+ class ThemeReader # only #color is reachable
505
172
  def color = AppConfig.theme.color
506
173
  end
507
174
 
508
- sandbox = Kobako::Sandbox.new
509
- sandbox.bind("Cfg::Settings", ThemeReader.new) # not: bind("Cfg::Settings", AppConfig)
510
-
511
- sandbox.eval('Cfg::Settings.color').value # => "#3366ff" — any other method raises Kobako::NoServiceError
175
+ sandbox.bind("Cfg::Settings", ThemeReader.new) # not AppConfig itself
512
176
  ```
513
177
 
514
- Guest code can name any `MyService::KV` path, but a forged name only resolves to
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.
519
-
520
178
  ## Performance
521
179
 
522
- Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values vary by hardware but ratios are stable across machines. Full numbers, methodology, and the +10%-regression gate live in [`benchmark/README.md`](benchmark/README.md).
523
-
524
- | Phase | Cost |
525
- |--------------------------------------------------------------|-----------------------|
526
- | First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
527
- | First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~3 ms one-time |
528
- | Subsequent `Sandbox.new` (caches warm) | ~3 µs |
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 |
180
+ Figures are order-of-magnitude on macOS arm64 with Ruby 3.4.7; ratios hold
181
+ across machines. The [benchmark] suite gates every release on a +10% regression.
534
182
 
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.
536
-
537
- #### Throughput Levers
538
-
539
- | Choice | What it changes |
183
+ | Phase | Cost |
540
184
  |---|---|
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) |
185
+ | first `Sandbox.new` per machine (JIT, then disk-cached) | ~500 ms |
186
+ | later `Sandbox.new` | ~3 µs |
187
+ | warm `#eval` with one Service call | ~62 µs |
544
188
 
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)).
546
-
547
- ```bash
548
- bundle exec rake bench # every gated regression benchmark (~5-8 min)
549
- ```
189
+ [benchmark]: https://github.com/elct9620/kobako/tree/main/benchmark
550
190
 
551
191
  ## Development
552
192
 
553
- After checking out the repo:
193
+ The release gate compiles everything and runs every test and lint.
554
194
 
555
195
  ```bash
556
- bin/setup # install dependencies
557
- bundle exec rake # the release gate: compile, tests, rubocop, steep, gate checks
196
+ bin/setup # install dependencies
197
+ bundle exec rake # the release gate
198
+ bin/console # IRB with the gem loaded
199
+ bundle exec rake install # install the local checkout
558
200
  ```
559
201
 
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.
202
+ The first compile builds mruby and the Guest Binary through the [beni] gem.
561
203
 
562
- ## Contributing
204
+ [beni]: https://github.com/elct9620/beni
563
205
 
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.
206
+ ## Contributing
565
207
 
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).
208
+ Bug reports and pull requests are welcome at the [repository]. Open an issue
209
+ before a non-trivial change, so we can agree on its scope.
567
210
 
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*` |
211
+ [repository]: https://github.com/elct9620/kobako
572
212
 
573
213
  ## License
574
214