kobako 0.20.0 → 0.21.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +107 -0
  4. data/Cargo.lock +12 -3
  5. data/README.md +96 -44
  6. data/ROADMAP.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +37 -0
  8. data/crates/kobako-runtime/Cargo.toml +8 -1
  9. data/crates/kobako-runtime/README.md +1 -1
  10. data/crates/kobako-runtime/src/dispatch.rs +17 -11
  11. data/crates/kobako-runtime/src/error.rs +38 -6
  12. data/crates/kobako-runtime/src/lib.rs +13 -2
  13. data/crates/kobako-runtime/src/profile.rs +8 -0
  14. data/crates/kobako-runtime/src/runtime.rs +7 -2
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -4
  16. data/crates/kobako-runtime/src/yielder.rs +4 -4
  17. data/crates/kobako-transport/CHANGELOG.md +31 -0
  18. data/crates/kobako-transport/Cargo.toml +27 -0
  19. data/crates/kobako-transport/README.md +43 -0
  20. data/crates/kobako-transport/src/abi.rs +89 -0
  21. data/crates/kobako-transport/src/envelope/bytes.rs +234 -0
  22. data/crates/kobako-transport/src/envelope/call.rs +222 -0
  23. data/crates/kobako-transport/src/envelope/error_record.rs +101 -0
  24. data/crates/kobako-transport/src/envelope/fault.rs +171 -0
  25. data/crates/kobako-transport/src/envelope/invocation_frames.rs +232 -0
  26. data/crates/kobako-transport/src/envelope/outcome.rs +261 -0
  27. data/crates/kobako-transport/src/envelope/reply.rs +240 -0
  28. data/crates/kobako-transport/src/envelope/run.rs +85 -0
  29. data/crates/kobako-transport/src/envelope.rs +73 -0
  30. data/crates/kobako-transport/src/lib.rs +21 -0
  31. data/crates/kobako-wasmtime/CHANGELOG.md +23 -0
  32. data/crates/kobako-wasmtime/Cargo.toml +5 -2
  33. data/crates/kobako-wasmtime/README.md +1 -1
  34. data/crates/kobako-wasmtime/src/config.rs +19 -12
  35. data/crates/kobako-wasmtime/src/dispatch.rs +15 -8
  36. data/crates/kobako-wasmtime/src/driver.rs +8 -27
  37. data/crates/kobako-wasmtime/src/frames.rs +11 -9
  38. data/crates/kobako-wasmtime/src/guest_mem.rs +7 -15
  39. data/crates/kobako-wasmtime/src/instance_pre.rs +2 -2
  40. data/crates/kobako-wasmtime/src/invocation.rs +1 -1
  41. data/crates/kobako-wasmtime/src/lib.rs +1 -1
  42. data/data/kobako.wasm +0 -0
  43. data/ext/kobako/Cargo.toml +2 -1
  44. data/ext/kobako/src/runtime/bridge.rs +78 -21
  45. data/ext/kobako/src/runtime/errors.rs +13 -5
  46. data/ext/kobako/src/runtime.rs +114 -35
  47. data/lib/kobako/catalog/services.rb +13 -21
  48. data/lib/kobako/catalog/snippets.rb +16 -34
  49. data/lib/kobako/codec/decoder.rb +10 -13
  50. data/lib/kobako/codec/encoder.rb +7 -7
  51. data/lib/kobako/codec/error.rb +6 -6
  52. data/lib/kobako/codec/ext_types.rb +12 -72
  53. data/lib/kobako/codec/handle_walk.rb +6 -6
  54. data/lib/kobako/codec/state.rb +4 -56
  55. data/lib/kobako/codec/utils.rb +4 -4
  56. data/lib/kobako/codec.rb +11 -21
  57. data/lib/kobako/context.rb +23 -17
  58. data/lib/kobako/errors.rb +32 -11
  59. data/lib/kobako/outcome.rb +71 -128
  60. data/lib/kobako/payload/arguments.rb +59 -0
  61. data/lib/kobako/payload.rb +19 -0
  62. data/lib/kobako/sandbox.rb +12 -13
  63. data/lib/kobako/sandbox_options.rb +1 -1
  64. data/lib/kobako/snippet/binary.rb +3 -4
  65. data/lib/kobako/snippet/source.rb +5 -6
  66. data/lib/kobako/snippet.rb +2 -2
  67. data/lib/kobako/transport/call.rb +21 -0
  68. data/lib/kobako/transport/dispatcher.rb +86 -135
  69. data/lib/kobako/transport/error.rb +21 -5
  70. data/lib/kobako/transport/reflection.rb +82 -0
  71. data/lib/kobako/transport/run.rb +25 -33
  72. data/lib/kobako/transport/yielder.rb +27 -36
  73. data/lib/kobako/transport.rb +11 -8
  74. data/lib/kobako/usage.rb +1 -1
  75. data/lib/kobako/version.rb +1 -1
  76. data/release-please-config.json +47 -2
  77. data/sig/kobako/catalog/services.rbs +1 -1
  78. data/sig/kobako/catalog/snippets.rbs +4 -4
  79. data/sig/kobako/codec/error.rbs +4 -4
  80. data/sig/kobako/codec/ext_types.rbs +0 -7
  81. data/sig/kobako/codec/state.rbs +0 -4
  82. data/sig/kobako/codec.rbs +4 -5
  83. data/sig/kobako/context.rbs +4 -4
  84. data/sig/kobako/errors.rbs +15 -3
  85. data/sig/kobako/outcome.rbs +12 -13
  86. data/sig/kobako/payload/arguments.rbs +26 -0
  87. data/sig/kobako/payload.rbs +4 -0
  88. data/sig/kobako/pool.rbs +0 -2
  89. data/sig/kobako/runtime.rbs +15 -13
  90. data/sig/kobako/snippet/binary.rbs +1 -1
  91. data/sig/kobako/snippet/source.rbs +1 -1
  92. data/sig/kobako/transport/call.rbs +21 -0
  93. data/sig/kobako/transport/dispatcher.rbs +8 -10
  94. data/sig/kobako/transport/error.rbs +11 -0
  95. data/sig/kobako/transport/reflection.rbs +15 -0
  96. data/sig/kobako/transport/run.rbs +1 -1
  97. data/sig/kobako/transport/yielder.rbs +2 -2
  98. data/sig/kobako/transport.rbs +8 -2
  99. metadata +23 -11
  100. data/lib/kobako/fault.rb +0 -38
  101. data/lib/kobako/outcome/panic.rb +0 -42
  102. data/lib/kobako/transport/request.rb +0 -76
  103. data/lib/kobako/transport/response.rb +0 -83
  104. data/lib/kobako/transport/yield.rb +0 -94
  105. data/sig/kobako/fault.rbs +0 -19
  106. data/sig/kobako/outcome/panic.rbs +0 -34
  107. data/sig/kobako/transport/request.rbs +0 -29
  108. data/sig/kobako/transport/response.rbs +0 -33
  109. data/sig/kobako/transport/yield.rbs +0 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c3d1c75e0967f01b1284848372d66adda8cc65566b828bb1634f1624b1b4938
4
- data.tar.gz: 223e0cc856970daf5c702775c1a3df2851d96beb72db1793e24c776747205a0c
3
+ metadata.gz: aa120ba374ca1b1ca531a99218645f73bca5bdf157fa40c053f4ee13297ac38e
4
+ data.tar.gz: 244a2791399621ba863fd23796c2b93bbd29a81606e48e700d51cdad140f968a
5
5
  SHA512:
6
- metadata.gz: 4b6050756d4ed75616ea4aa82fe56febc11cc97c8b1cba165c607e70b6921dfad322c1594746b3e105338653fce93f5e208b4f2516c39f2f3cb4d9b85647b79b
7
- data.tar.gz: 8c50704f755219097efdd41c5e47ba304fa98fe8843e5f39ee49df4c22cac7d7916cd8a96be472ffae3535d4f285422e0cca81917c722584b44165f262611212
6
+ metadata.gz: 58093ae0a28a1018af3479465647ea5ef5eaf61aadfdbc42cbcd7904d310015363d7aa414bf6d4073b06967d878dcce139327329cb7e9c9d21e4ce9abe2ab22b
7
+ data.tar.gz: 22bd8b84e48e2d49f27202353998cf80d207b2f3e0c4ae748cd95676f4a444c6d69623a87412c94cbe9658b0b08741b8f11c85d10a13371639c1d669075a53de
@@ -1 +1 @@
1
- {".":"0.20.0","wasm/kobako-core":"0.12.0","wasm/kobako-mruby":"0.12.0","wasm/kobako-io":"0.12.0","wasm/kobako-json":"0.12.0","wasm/kobako-regexp":"0.12.0","wasm/kobako-baker":"0.12.0","crates/kobako-codec":"0.12.0","crates/kobako-runtime":"0.12.0","crates/kobako-wasmtime":"0.12.0","crates/kobako":"0.12.0"}
1
+ {".":"0.21.0","wasm/kobako-core":"0.13.0","wasm/kobako-mruby":"0.13.0","wasm/kobako-io":"0.13.0","wasm/kobako-json":"0.13.0","wasm/kobako-regexp":"0.13.0","wasm/kobako-baker":"0.13.0","crates/kobako-transport":"0.13.0","crates/kobako-codec":"0.13.0","crates/kobako-runtime":"0.13.0","crates/kobako-wasmtime":"0.13.0","crates/kobako":"0.13.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,112 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.21.0](https://github.com/elct9620/kobako/compare/v0.20.0...v0.21.0) (2026-07-29)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **spec:** speak one word for the arm every envelope answers success on
9
+ * **transport:** let every envelope spell its success arm the same way
10
+ * **spec:** separate what a name promises from what the wire promises
11
+ * **sdk:** keep the root to what every build of this crate has
12
+ * **sdk:** make the wasm engine something a host can actually take out
13
+ * **mruby:** name each codec method after the position it serves
14
+ * **wasmtime:** put every cap in one struct, named the way the field is read
15
+ * **runtime:** give the engine contract names an implementer can write
16
+ * **transport:** name the core envelope's types after what they carry
17
+ * **guest:** make a parked block and its wire bit one statement
18
+ * **sdk:** reach a result's host object without a schema
19
+ * **guest:** let the shell ask for the codec instead of inheriting it
20
+ * **customization:** the harness carries no codec until a shell asks
21
+ * **sdk:** make the payload codec an optional dependency
22
+ * **wire:** carry a Reply's fault arm on the envelope
23
+ * **wire:** move the Reply's fault arm onto the envelope
24
+ * **transport:** split the invocation module along the line its doc draws
25
+ * **wire:** route every tier through the one envelope
26
+ * **gem:** follow Ruby's own convention for what an error is called
27
+ * **guest:** name the payload seam for what it is — a codec
28
+ * **wire:** keep a Fault to what its author can bound
29
+ * **wire:** give the fault concept one name on both sides of the boundary
30
+ * **guest:** let the shell name the schema its guest speaks
31
+ * **guest:** route a dispatch through kobako-core without reading its payload
32
+ * **outcome:** raise an unresolved entrypoint as its own error carrying the names it could have been
33
+ * **outcome:** keep a Panic's attribution when its diagnostics are unreadable
34
+ * **wire:** carry the Outcome over the core envelope
35
+ * **wire:** carry the Yield Reply over the core envelope
36
+ * **wire:** carry the Run envelope over the core envelope
37
+ * **wire:** carry the invocation frames over the core envelope
38
+ * **codec:** make the MessagePack adapter an optional feature
39
+ * **wire:** carry dispatch over the core envelope with an opaque payload
40
+
41
+ ### Features
42
+
43
+ * **bench:** measure the host's per-invocation cost against a null guest ([c60abdf](https://github.com/elct9620/kobako/commit/c60abdfc98a38b03a4a598913df9a1120ed6302b))
44
+ * **bench:** move the two host-side benchmarks into the gated set ([31a6f7e](https://github.com/elct9620/kobako/commit/31a6f7ebb40c038b39c38ee2441216a412be48d0))
45
+ * **codec:** make the MessagePack adapter an optional feature ([effff27](https://github.com/elct9620/kobako/commit/effff278fd220a4d3c8d4e857828d610fe01f48e))
46
+ * **guest:** let the shell name the schema its guest speaks ([5ad2e0d](https://github.com/elct9620/kobako/commit/5ad2e0db4c645ab56153a81d1623e37e8cd8f5c6))
47
+ * **guest:** open the block seam to a capability gem ([c6d6e49](https://github.com/elct9620/kobako/commit/c6d6e49013bd0f850dd4f2c17273103f166294c2))
48
+ * **outcome:** keep a Panic's attribution when its diagnostics are unreadable ([8accd40](https://github.com/elct9620/kobako/commit/8accd404c9d9155f19fa0458d7e0650b5d587eb1))
49
+ * **outcome:** raise an unresolved entrypoint as its own error carrying the names it could have been ([05b4125](https://github.com/elct9620/kobako/commit/05b41257ca8d4bcb90d6759c6cc7b20582af0661))
50
+ * **sdk:** let a host bring its own wasm engine ([70c2475](https://github.com/elct9620/kobako/commit/70c24759ef4cd5785bd1418a6f8d199620f59e2b))
51
+ * **sdk:** make the payload codec an optional dependency ([0c7da71](https://github.com/elct9620/kobako/commit/0c7da71bfc0b138a316c9612b23b2944684d652e))
52
+ * **sdk:** make the wasm engine something a host can actually take out ([c9f8de6](https://github.com/elct9620/kobako/commit/c9f8de662e6dcd31c8444d9424b1a87ca144f1a6))
53
+ * **sdk:** reach a result's host object without a schema ([a0a2c56](https://github.com/elct9620/kobako/commit/a0a2c56ca5efe8daded876cdcfb8cf0ec8dd4e77))
54
+ * **spec:** anchor what a guest does at a position its codec does not serve ([3a084ea](https://github.com/elct9620/kobako/commit/3a084ea3c60c193038b6dd06d191b14d2ccde4f1))
55
+ * **tasks:** gate signatures against declarations the code dropped ([8b1b834](https://github.com/elct9620/kobako/commit/8b1b834664a51ed6330ba77e9cc3fb7cc4490faf))
56
+ * **tasks:** gate the benchmark probes on still running ([31c3455](https://github.com/elct9620/kobako/commit/31c345534c36f49670075fbfdecff645314d6d24))
57
+ * **wire:** carry a Reply's fault arm on the envelope ([0bce850](https://github.com/elct9620/kobako/commit/0bce850b3416696e92ae6ee12c353c4c21c8e583))
58
+ * **wire:** carry dispatch over the core envelope with an opaque payload ([556104b](https://github.com/elct9620/kobako/commit/556104bf86fdf481b5368b70af83eb0add4b2708))
59
+ * **wire:** carry the invocation frames over the core envelope ([b6f266a](https://github.com/elct9620/kobako/commit/b6f266a310ff723a1868cff745763d91c0603a5e))
60
+ * **wire:** carry the Outcome over the core envelope ([3fa338e](https://github.com/elct9620/kobako/commit/3fa338e4e03b16c4f25f903f1d45f672ab1d015d))
61
+ * **wire:** carry the Run envelope over the core envelope ([d7c46ab](https://github.com/elct9620/kobako/commit/d7c46ab64e382f5226c780b39c8bb66ee12213ac))
62
+ * **wire:** carry the Yield Reply over the core envelope ([0db7261](https://github.com/elct9620/kobako/commit/0db726189adbc369e32b569cd191861dbba17302))
63
+
64
+
65
+ ### Bug Fixes
66
+
67
+ * **bench:** bound and surface the band the archive widens ([cc2ddd2](https://github.com/elct9620/kobako/commit/cc2ddd213d8f9382c9b7ff20581abd2b171fe486))
68
+ * **bench:** let probe discovery reach any depth ([2a164ca](https://github.com/elct9620/kobako/commit/2a164cab782a51e9d15b522f8a2c340b5f234b9a))
69
+ * **bench:** name only the rows the archive actually sets the bar for ([68e167f](https://github.com/elct9620/kobako/commit/68e167f56c801ad4b9caf7f290824d28f216aec9))
70
+ * **bench:** sample the cold-start row's guest budget instead of observing it once ([95bdadd](https://github.com/elct9620/kobako/commit/95bdadde1f81fb169293c89cbccd460fb0c6e502))
71
+ * **bench:** widen the gate's noise band by what the archive shows ([183407c](https://github.com/elct9620/kobako/commit/183407cfc3854c1179f7c5b7da338d2295526bfc))
72
+ * **build:** rebuild the guest when the fixed tier changes ([037cc2e](https://github.com/elct9620/kobako/commit/037cc2e9451fbf34bd2138abfbc8b9b59fd613cc))
73
+ * **ext:** name the Panic attribution tuple and drop two lint carries ([ab24625](https://github.com/elct9620/kobako/commit/ab24625233c3559c9b3c737182d6614a4e03926b))
74
+ * **guest:** make a parked block and its wire bit one statement ([dba5625](https://github.com/elct9620/kobako/commit/dba5625c541c9b54caa89f5937419ec416ec05a8))
75
+ * **guest:** read a String's bytes instead of rendering it ([de8c238](https://github.com/elct9620/kobako/commit/de8c238e900830f72d374f83d8c8e5b63cef4384))
76
+ * **guest:** refuse text the capability gems cannot read as text ([539045a](https://github.com/elct9620/kobako/commit/539045a80192cc28c23a4bcbebc311c55eb138fb))
77
+ * **payload:** stop claiming a zero-copy decode the args position cannot have ([c56cb9c](https://github.com/elct9620/kobako/commit/c56cb9c30dc727d9ad0510e3f1572c1ab53463d9))
78
+ * **release:** drop the version from a dev-dependency nothing rewrites ([524e536](https://github.com/elct9620/kobako/commit/524e5365448fedb101571a11de546dddebbfbcb6))
79
+ * **sig:** declare the dispatch seam the shape the ext actually calls ([f0de465](https://github.com/elct9620/kobako/commit/f0de46500eda366d3edd7869edcc39d843f4b687))
80
+ * **sig:** drop the Pool method the signature outlived ([35a5b4f](https://github.com/elct9620/kobako/commit/35a5b4ff3e834b157d695819272c2b445f0bb5bf))
81
+ * **tasks:** let a retired anchor hold the ceiling it was assigned ([a9cd51d](https://github.com/elct9620/kobako/commit/a9cd51d2bd454698833024c89c3693b0d73ef505))
82
+ * **wire:** keep the ABI at 3, which no release has shipped ([2b301b7](https://github.com/elct9620/kobako/commit/2b301b777384c0edf79426805cfb1cc08688aa95))
83
+
84
+
85
+ ### Documentation
86
+
87
+ * **customization:** the harness carries no codec until a shell asks ([a9bf10c](https://github.com/elct9620/kobako/commit/a9bf10c20f27f52183932dd149768626d70f31bf))
88
+ * **spec:** separate what a name promises from what the wire promises ([545bfbd](https://github.com/elct9620/kobako/commit/545bfbd59834bab6d564850d9adafa892cbae005))
89
+ * **spec:** speak one word for the arm every envelope answers success on ([f4ef5f5](https://github.com/elct9620/kobako/commit/f4ef5f5c096cb2c5b0153f7a28f7feaf15a405d0))
90
+ * **wire:** move the Reply's fault arm onto the envelope ([3a56e89](https://github.com/elct9620/kobako/commit/3a56e892caea5b2024baeeeff0bcb8b0169e9b16))
91
+
92
+
93
+ ### Code Refactoring
94
+
95
+ * **gem:** follow Ruby's own convention for what an error is called ([b1ce1fe](https://github.com/elct9620/kobako/commit/b1ce1fe994e5123a950ba9b9f64810387822bb46))
96
+ * **guest:** let the shell ask for the codec instead of inheriting it ([abc24d3](https://github.com/elct9620/kobako/commit/abc24d3321c614f36f8c01c932169083796a0915))
97
+ * **guest:** name the payload seam for what it is — a codec ([b5d90e2](https://github.com/elct9620/kobako/commit/b5d90e26cdea1e0048cb44e48c6543a6bad4a592))
98
+ * **guest:** route a dispatch through kobako-core without reading its payload ([62c5791](https://github.com/elct9620/kobako/commit/62c5791eb88b9021b3d60c8a3dd45d5907011214))
99
+ * **mruby:** name each codec method after the position it serves ([e2d4b53](https://github.com/elct9620/kobako/commit/e2d4b53dd2d165d4f6312c8e87a5dd936d2f7a2f))
100
+ * **runtime:** give the engine contract names an implementer can write ([0a41491](https://github.com/elct9620/kobako/commit/0a41491360cf17def8245c0758679e92572b990f))
101
+ * **sdk:** keep the root to what every build of this crate has ([7f93096](https://github.com/elct9620/kobako/commit/7f93096d4371e7e7f70f3b31301fad3a3198184a))
102
+ * **transport:** let every envelope spell its success arm the same way ([df595e0](https://github.com/elct9620/kobako/commit/df595e08097af9e3e0a0c5ed52a4a6f854eb21dc))
103
+ * **transport:** name the core envelope's types after what they carry ([b44de65](https://github.com/elct9620/kobako/commit/b44de6502953826c567d4ef8a594561479f50c1d))
104
+ * **transport:** split the invocation module along the line its doc draws ([4d3a2d3](https://github.com/elct9620/kobako/commit/4d3a2d3060167d71a807e203688186740fd1485b))
105
+ * **wasmtime:** put every cap in one struct, named the way the field is read ([3f57860](https://github.com/elct9620/kobako/commit/3f578608b044d1ade0ae35231266cf3c9e517c02))
106
+ * **wire:** give the fault concept one name on both sides of the boundary ([564798a](https://github.com/elct9620/kobako/commit/564798a9662f547f665530a75d99c73051a79d86))
107
+ * **wire:** keep a Fault to what its author can bound ([141209d](https://github.com/elct9620/kobako/commit/141209df0f5525853462fddcfaf3584ea530038b))
108
+ * **wire:** route every tier through the one envelope ([c5cd33a](https://github.com/elct9620/kobako/commit/c5cd33a5346f49857fa6e1f45c9cf9b9bea0ff77))
109
+
3
110
  ## [0.20.0](https://github.com/elct9620/kobako/compare/v0.19.0...v0.20.0) (2026-07-24)
4
111
 
5
112
 
data/Cargo.lock CHANGED
@@ -929,9 +929,10 @@ dependencies = [
929
929
 
930
930
  [[package]]
931
931
  name = "kobako"
932
- version = "0.20.0"
932
+ version = "0.21.0"
933
933
  dependencies = [
934
934
  "kobako-runtime",
935
+ "kobako-transport",
935
936
  "kobako-wasmtime",
936
937
  "magnus",
937
938
  "rb-sys",
@@ -939,13 +940,21 @@ dependencies = [
939
940
 
940
941
  [[package]]
941
942
  name = "kobako-runtime"
942
- version = "0.12.0"
943
+ version = "0.13.0"
944
+ dependencies = [
945
+ "kobako-transport",
946
+ ]
947
+
948
+ [[package]]
949
+ name = "kobako-transport"
950
+ version = "0.13.0"
943
951
 
944
952
  [[package]]
945
953
  name = "kobako-wasmtime"
946
- version = "0.12.0"
954
+ version = "0.13.0"
947
955
  dependencies = [
948
956
  "kobako-runtime",
957
+ "kobako-transport",
949
958
  "libc",
950
959
  "sha2 0.11.0",
951
960
  "wasmtime",
data/README.md CHANGED
@@ -14,9 +14,10 @@ The host (`wasmtime`) runs a precompiled `kobako.wasm` guest containing mruby an
14
14
  │ Services │ ◀─call─ │ KV::Lookup.call(k) │
15
15
  │ KV::Lookup │ ─resp─▶ │ │
16
16
  │ │ │ │
17
- stdout / stderr buf │ ◀─pipe─ │ puts / warn │
18
-
19
- return value │ ◀─last─ last expression
17
+ Execution │ ◀─pipe─ │ puts / warn │
18
+ value ◀─last─ last expression
19
+ stdout / stderr
20
+ │ usage │ │ │
20
21
  └──────────────────────┘ └──────────────────────┘
21
22
  trusted untrusted
22
23
  ```
@@ -45,23 +46,27 @@ The same untrusted script runs identically from either host frontend. It execute
45
46
  require "kobako"
46
47
 
47
48
  sandbox = Kobako::Sandbox.new
48
- sandbox.eval("1 + 2") # => 3
49
+ sandbox.eval("1 + 2").value # => 3
49
50
  ```
50
51
 
51
52
  ```rust
52
53
  use kobako::{Options, Sandbox};
53
54
 
54
55
  // 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)
56
+ let sandbox = Sandbox::new("kobako.wasm", Options::default())?;
57
+ sandbox.eval("1 + 2")?.value()?; // => Value::Int(3)
57
58
  ```
58
59
 
60
+ Each invocation returns the record of that one run — `Kobako::Execution` in Ruby, `Execution` in Rust — carrying the guest value alongside the run's output and resource usage. Nothing a run observes is stored on the Sandbox, so the same Sandbox serves concurrent invocations without them seeing each other.
61
+
59
62
  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
63
 
61
64
  ## Frontends & Guest Binaries
62
65
 
63
66
  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
67
 
68
+ How far down those choices go — and which ones a given starting point quietly makes for you — is laid out in [`docs/architecture.md`](docs/architecture.md).
69
+
65
70
  ### Host frontends
66
71
 
67
72
  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.
@@ -69,8 +74,8 @@ The host embeds the sandbox and owns the SPEC wire codec. Choose by your host la
69
74
  | Frontend | Package | Add it | Best for |
70
75
  |----------|---------|--------|----------|
71
76
  | Ruby gem | `kobako` (RubyGems) | `gem install kobako` | A Ruby host — Services, Handles, snippets, and pooling out of the box |
72
- | Rust SDK | `kobako` (crates.io) | `kobako = "0.9"` | A Rust host — the same behavior contract behind an idiomatic Rust API |
73
- | Low-level crates | `kobako-wasmtime` + `kobako-runtime` + `kobako-codec` | Cargo deps | A custom host, or driving the wire directly in another language |
77
+ | Rust SDK | `kobako` (crates.io) | `kobako = "0.12"` | A Rust host — the same behavior contract behind an idiomatic Rust API |
78
+ | Low-level crates | `kobako-wasmtime` + `kobako-runtime` + `kobako-transport` + `kobako-codec` | Cargo deps | A custom host, or driving the wire directly in another language |
74
79
 
75
80
  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
81
 
@@ -88,7 +93,7 @@ The gem bundles the pure `kobako.wasm`. Regexp and JSON are opt-in capabilities
88
93
 
89
94
  ```ruby
90
95
  sandbox = Kobako::Sandbox.new(wasm_path: "kobako+full.wasm")
91
- sandbox.eval('JSON.generate({ n: "42".to_i })') # => "{\"n\":42}"
96
+ sandbox.eval('JSON.generate({ n: "42".to_i })').value # => "{\"n\":42}"
92
97
  ```
93
98
 
94
99
  ### Custom Guest Binaries
@@ -133,9 +138,11 @@ Build the crate as a `cdylib` for `wasm32-wasip1`, then bake the canonical boot
133
138
 
134
139
  | Term | Meaning |
135
140
  |------|---------|
136
- | Sandbox | The runtime unit (`Kobako::Sandbox`) that runs guest code and returns a result or raises a typed error. |
141
+ | Sandbox | The reusable unit (`Kobako::Sandbox`) that runs guest code and returns a result or raises a typed error. It holds configuration only — no state from any run. |
137
142
  | Service | A host object bound at a constant-path name (`MyService::KV`) — the guest's only path to host resources. |
138
- | Invocation | One `#eval` or `#run`; capability state resets between invocations. |
143
+ | Invocation | One `#eval` or `#run`; capability state is scoped to it and ends with it. |
144
+ | Execution | The frozen record one invocation returns (`Kobako::Execution`): its `#value`, output captures, and `#usage`. A failed run raises, carrying the same record on the error's `#execution`. |
145
+ | Context | The per-invocation object the optional `#eval` / `#run` block receives; its `ctx.bind` supplies a Service object for that one run. |
139
146
  | Snippet | Named mruby code (source or bytecode) replayed into a fresh state before every invocation. |
140
147
  | Handle | An opaque token the guest holds for a host object the wire cannot transmit directly. |
141
148
  | Block | A guest mruby block passed to a Service; each `yield` is a synchronous round-trip into the guest. |
@@ -166,25 +173,37 @@ RUBY
166
173
 
167
174
  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`.
168
175
 
176
+ ### Per-Invocation Bindings
177
+
178
+ A setup-time `bind` fixes one object for the Sandbox's life. When the object belongs to a single run instead — the current request, the acting user, a per-tenant store — 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 ([`docs/behavior/registration.md`](docs/behavior/registration.md) B-62..B-63).
179
+
180
+ ```ruby
181
+ sandbox.bind("Req::Current") # declared, unfilled — stands for Kobako::Unresolved
182
+
183
+ sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
184
+ ```
185
+
186
+ `ctx.bind` also shadows an already-bound path for that one run. The Context is spent once the block returns, so a `ctx` captured out of it raises `ArgumentError`. Choosing the object per invocation instead of sharing one is what lets concurrent Threads invoke a single Sandbox and still keep their identities apart — the [multi-tenant example](examples/multi-tenant/) runs that shape end to end.
187
+
169
188
  ### Output Capture
170
189
 
171
- Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and exposed independently of the return value ([`docs/behavior/lifecycle.md`](docs/behavior/lifecycle.md) B-04). Buffers clear at the start of each invocation; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
190
+ Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and read off the run's Execution, independently of its `#value` ([`docs/behavior/lifecycle.md`](docs/behavior/lifecycle.md) B-04). Each invocation captures its own; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
172
191
 
173
192
  ```ruby
174
- result = sandbox.eval(<<~RUBY)
193
+ execution = sandbox.eval(<<~RUBY)
175
194
  puts "hello"
176
195
  warn "be careful"
177
196
  42
178
197
  RUBY
179
198
 
180
- result # => 42
181
- sandbox.stdout # => "hello\n"
182
- sandbox.stderr # => "be careful\n"
199
+ execution.value # => 42
200
+ execution.stdout # => "hello\n"
201
+ execution.stderr # => "be careful\n"
183
202
  ```
184
203
 
185
204
  ### Error Handling
186
205
 
187
- Every invocation either returns a value 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
+ 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).
188
207
 
189
208
  ```ruby
190
209
  begin
@@ -193,23 +212,27 @@ rescue Kobako::TrapError
193
212
  # Wasm engine fault or cap exhaustion. Discard the Sandbox.
194
213
  rescue Kobako::ServiceError
195
214
  # A host Service call failed and the script did not rescue it.
196
- rescue Kobako::SandboxError
215
+ rescue Kobako::SandboxError => e
197
216
  # The script raised, failed to compile, or returned an unrepresentable value.
217
+ logger.warn(e.execution.stderr) # the failed run's own captures and usage
198
218
  end
199
219
  ```
200
220
 
221
+ Each of these carries the failed run's Execution on `#execution`, so 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
+
201
223
  | Class | Parent | Trigger |
202
224
  |---------------------------------|----------------|------------------------------------------------------|
203
225
  | `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
204
226
  | `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
205
227
  | `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
206
228
  | `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
229
+ | `Kobako::UndefinedEntrypointError` | `SandboxError` | `#run` named a constant no snippet defined; carries `#name` and `#available` |
207
230
 
208
- `SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` / `details` fields when the guest produced a panic envelope.
231
+ `SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` fields when the guest produced a panic envelope.
209
232
 
210
233
  ### Resource Limits
211
234
 
212
- 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 [`Sandbox#usage`](lib/kobako/sandbox.rb) after the call — populated on every outcome including traps for actual consumption ([`docs/behavior/lifecycle.md`](docs/behavior/lifecycle.md) B-35).
235
+ Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Execution#usage`](lib/kobako/execution.rb) for actual consumption — populated on every outcome, so a rescued trap reports it just as a completed run does ([`docs/behavior/lifecycle.md`](docs/behavior/lifecycle.md) B-35).
213
236
 
214
237
  ```ruby
215
238
  sandbox = Kobako::Sandbox.new(
@@ -229,17 +252,30 @@ sandbox = Kobako::Sandbox.new(
229
252
 
230
253
  `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.
231
254
 
232
- A fifth option, `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, with filesystem, environment, and network still unreachable. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/security-model.md`](docs/security-model.md) § Isolation profiles.
255
+ Beyond the four caps, `profile:` requests the Sandbox's isolation posture on the `:permissive` < `:hermetic` ladder (default `:hermetic`). `:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources — an explicit trade of reproducibility, with filesystem, environment, and network still unreachable. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/security-model.md`](docs/security-model.md) § Isolation profiles.
256
+
257
+ ### Concurrency
258
+
259
+ 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 ([`docs/behavior/runtime.md`](docs/behavior/runtime.md) B-22). One Thread still runs one invocation at a time. Sharing a Sandbox adds a single obligation: an object bound once at setup is reached by every Thread and must itself be thread-safe, while an object supplied per invocation — `ctx.bind`, or an Extension `provider:` — carries no such requirement.
260
+
261
+ 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 (B-64).
262
+
263
+ ```ruby
264
+ sandbox = Kobako::Sandbox.new(gvl: :release)
265
+ ```
266
+
267
+ The mode is per-Sandbox and fixed at construction; it changes scheduling only, leaving isolation, Handle lifetimes, captures, and outcomes identical. `:hold` remains the default because releasing pays a handoff cost at every dispatch: 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.
233
268
 
234
269
  ### Invocation Lifecycle
235
270
 
236
- One Sandbox serves many invocations. Service bindings and preloaded snippets persist across calls; capability state (Handles, stdout, stderr, memory delta) resets between them.
271
+ 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.
237
272
 
238
273
  ```
239
274
  ───────────── setup phase (mutable) ─────────────
240
275
 
241
276
  sandbox = Kobako::Sandbox.new
242
277
  sandbox.bind("KV::Lookup", ...)
278
+ sandbox.bind("Req::Current") # fillable: object comes later
243
279
  sandbox.preload(code: ..., name: :Adder)
244
280
  sandbox.preload(code: ..., name: :Greeter)
245
281
 
@@ -256,20 +292,24 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
256
292
 
257
293
  ──────────────── invocation N ───────────────────
258
294
 
259
- 1. start from the canonical boot state
295
+ 1. resolve this run's bindings, before the guest starts:
296
+ ctx.bind("Req::Current", ...) fills the fillable
297
+ provider: backends yield this run's object
298
+
299
+ 2. start from the canonical boot state
260
300
  (mruby pre-initialized into the artifact at build time)
261
301
 
262
- 2. replay snippets (in insertion order):
302
+ 3. replay snippets (in insertion order):
263
303
  :Adder → defines Adder
264
304
  :Greeter → defines Greeter
265
305
 
266
- 3. dispatch: eval(source) or run(:Target, *args, **kwargs)
306
+ 4. dispatch: eval(source) or run(:Target, *args, **kwargs)
267
307
 
268
- 4. return value to host
308
+ 5. return the run's Execution to host
269
309
 
270
- 5. discard the instance; reset per-invocation state:
310
+ 6. discard the instance; per-invocation state ends with it:
271
311
  · Handles invalidated
272
- · stdout / stderr buffers cleared
312
+ · captures frozen into the Execution
273
313
  · memory delta zeroed
274
314
 
275
315
  Services + snippets persist; invocation N+1 repeats.
@@ -281,14 +321,14 @@ For workloads that must be isolated from each other (one Sandbox per tenant, per
281
321
 
282
322
  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.
283
323
 
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).
324
+ `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). What a Pool buys is warm setup and exclusive checkout, not isolation: a Sandbox holds no state from any run, so Threads sharing one are equally safe (see [Concurrency](#concurrency)).
285
325
 
286
326
  ```ruby
287
327
  pool = Kobako::Pool.new(slots: 4) do |sandbox|
288
328
  sandbox.bind("KV::Lookup", ->(key) { redis.get(key) })
289
329
  end
290
330
 
291
- pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))) }
331
+ pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))).value }
292
332
  ```
293
333
 
294
334
  | Option | Meaning | Default |
@@ -296,7 +336,7 @@ pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))) }
296
336
  | `slots:` | Upper bound on constructed Sandboxes | required |
297
337
  | `checkout_timeout:` | Seconds `#with` waits for a free Sandbox; `nil` waits indefinitely | 5.0 |
298
338
 
299
- Sandboxes construct lazily on first demand. `#with` yields a Sandbox with empty output buffers and returns the block's value; at block exit the Sandbox returns to the pool, except a block that raises `Kobako::TrapError` discards its Sandbox and the slot refills by a fresh construction on next demand. A checkout that waits past `checkout_timeout` raises `Kobako::PoolTimeoutError`. There is no teardown verb — a Pool releases everything with its own reachability.
339
+ Sandboxes construct lazily on first demand. `#with` yields a Sandbox and returns the block's value; at block exit the Sandbox returns to the pool, except a block that raises `Kobako::TrapError` discards its Sandbox and the slot refills by a fresh construction on next demand. A checkout that waits past `checkout_timeout` raises `Kobako::PoolTimeoutError`. There is no teardown verb — a Pool releases everything with its own reachability.
300
340
 
301
341
  ### Service Blocks
302
342
 
@@ -305,7 +345,7 @@ A Service method can accept a guest-supplied block via `&blk` and `yield` into i
305
345
  ```ruby
306
346
  sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
307
347
 
308
- sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }')
348
+ sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }').value
309
349
  # => [2, 4, 6]
310
350
  ```
311
351
 
@@ -321,8 +361,8 @@ end
321
361
 
322
362
  sandbox.bind("Factory::Make", ->(name) { Greeter.new(name) })
323
363
 
324
- sandbox.eval('Factory::Make.call("Bob").greet') # => "hi, Bob" (Handle round-trip inside guest)
325
- sandbox.eval('Factory::Make.call("Bob")') # => #<Greeter @name="Bob"> (B-37 restoration)
364
+ sandbox.eval('Factory::Make.call("Bob").greet').value # => "hi, Bob" (Handle round-trip inside guest)
365
+ sandbox.eval('Factory::Make.call("Bob")').value # => #<Greeter @name="Bob"> (B-37 restoration)
326
366
  ```
327
367
 
328
368
  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.
@@ -351,13 +391,17 @@ This is deliberate, not a leak. Handle IDs run to 2³¹ − 1 per invocation and
351
391
 
352
392
  ```ruby
353
393
  sandbox = Kobako::Sandbox.new
354
- sandbox.preload(code: "Adder = ->(a, b) { a + b }", name: :Adder)
355
- sandbox.preload(code: 'Greeter = ->(name:) { "hello, #{name}" }', name: :Greeter)
394
+ sandbox.preload(code: "Adder = ->(a, b) { a + b }", name: :Adder)
395
+ sandbox.preload(code: 'Greeter = ->(opts) { "hello, #{opts[:name]}" }', name: :Greeter)
356
396
 
357
- sandbox.run(:Adder, 2, 3) # => 5
358
- sandbox.run(:Greeter, name: "world") # => "hello, world"
397
+ sandbox.run(:Adder, 2, 3).value # => 5
398
+ sandbox.run(:Greeter, name: "world").value # => "hello, world"
359
399
  ```
360
400
 
401
+ An entrypoint's `kwargs` arrive as a trailing positional Hash — mruby's C-side call path carries no keyword arguments — so declare a Hash parameter and unpack it yourself.
402
+
403
+ A target no snippet defined raises `Kobako::UndefinedEntrypointError`, whose `#available` lists the top-level constants the snippets did contribute — so the name is corrected from the error rather than by reading the guest source.
404
+
361
405
  ```
362
406
  per-invocation replay (every #eval / #run, snippets in insertion order):
363
407
 
@@ -370,7 +414,7 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
370
414
  └──▶ eval(source) -or- run(:Target, *args, **kwargs)
371
415
 
372
416
 
373
- return value, then instance discarded
417
+ return the Execution, then discard the instance
374
418
  ```
375
419
 
376
420
  `#preload` accepts two payload forms:
@@ -401,7 +445,7 @@ sandbox.install(
401
445
  source: FILE,
402
446
  backend: Kobako::Extension::Backend.new(
403
447
  path: "File",
404
- provider: -> { OverlayFileSystem.new(root) } # callable fresh per invocation
448
+ provider: -> { OverlayFileSystem.new(root) } # invoked once per invocation
405
449
  )
406
450
  )
407
451
  )
@@ -409,7 +453,15 @@ sandbox.install(
409
453
  sandbox.eval('File.read("sample.txt")') # dispatches to the backend's #read
410
454
  ```
411
455
 
412
- The `backend.provider` sets the bound object's lifetime: a fixed object is shared across every invocation, while a callable is invoked once per invocation to yield a fresh object the form a writable backend needs, so its state cannot leak across calls. 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.
456
+ A backend declares the bound object's lifetime by keyword, never by inference — so a static object that happens to be callable stays unambiguous.
457
+
458
+ | Keyword | Lifetime |
459
+ |-------------|-----------------------------------------------------------------------------|
460
+ | `object:` | One object, shared by every invocation |
461
+ | `provider:` | A no-argument callable invoked per invocation — what a writable backend needs, so its state cannot leak across runs |
462
+ | neither | A fillable, standing for `Kobako::Unresolved` until `ctx.bind` supplies the run's object |
463
+
464
+ kobako ships no concrete Extension; the idiom and backend are yours. The [overlay VFS example](examples/vfs/) is a worked `File` that reads through to disk while protecting it from guest writes.
413
465
 
414
466
  ## Security
415
467
 
@@ -425,7 +477,7 @@ end
425
477
  sandbox = Kobako::Sandbox.new
426
478
  sandbox.bind("Cfg::Settings", ThemeReader.new) # not: bind("Cfg::Settings", AppConfig)
427
479
 
428
- sandbox.eval('Cfg::Settings.color') # => "#3366ff" — every other method raises NoMethodError
480
+ sandbox.eval('Cfg::Settings.color').value # => "#3366ff" — every other method raises NoMethodError
429
481
  ```
430
482
 
431
483
  When a purpose-built wrapper is more than you need, an object can gate its own surface in
@@ -454,12 +506,12 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
454
506
  | Snippet replay per invocation | ~7.6 µs each |
455
507
  | Per additional idle Sandbox (RSS) | ~1 KB |
456
508
 
457
- 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.
509
+ 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. Under the default `gvl: :hold`, wasm work is GVL-serialized: aggregate throughput stays around 17k `#eval`/s regardless of Thread count, though Ruby-side `#eval` setup still overlaps. Opting a Sandbox into `gvl: :release` lifts that ceiling for compute-bound scripts (see [Concurrency](#concurrency)). A +10% regression on any SPEC-mandated benchmark blocks release.
458
510
 
459
511
  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)). There `=~` (~5 µs/match) costs about 4× `match?` (~1.2 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.
460
512
 
461
513
  ```bash
462
- bundle exec rake bench # six gated regression benchmarks (~5-8 min)
514
+ bundle exec rake bench # every gated regression benchmark (~5-8 min)
463
515
  ```
464
516
 
465
517
  ## Development
data/ROADMAP.md CHANGED
@@ -16,7 +16,7 @@ output capture, and a warm Sandbox pool.
16
16
  | ✅ [F-06 Capability Handle encoding and referencing](docs/behavior/dispatch.md) | [lib/kobako/catalog/handles.rb](lib/kobako/catalog/handles.rb) | — |
17
17
  | ✅ [F-07 Three-class error attribution and raising](docs/behavior/errors.md) | [lib/kobako/outcome.rb](lib/kobako/outcome.rb) | E-26 (guest-entry envelope decode failure) has no exercising test — not reachable through the public API |
18
18
  | ✅ [F-08 Guest output capture](docs/behavior/lifecycle.md) | [lib/kobako/capture.rb](lib/kobako/capture.rb) | — |
19
- | ✅ [F-09 Host–guest message codec](docs/wire-codec.md) | [lib/kobako/codec/](lib/kobako/codec/) | |
19
+ | ✅ [F-09 Host–guest message codec](docs/wire-codec.md) | [crates/kobako-transport/](crates/kobako-transport/) (core envelope + ABI, one implementation), [lib/kobako/codec/](lib/kobako/codec/) (host payload codec) | The payload layer has a second implementation in `crates/kobako-codec`; the envelope layer is pinned by golden vectors instead |
20
20
  | ✅ [F-10 Reproducible build pipeline](SPEC.md#code-organization) | [tasks/wasm/build.rake](tasks/wasm/build.rake) | Verified by build-time gates (`rake anchors`, double-bake byte-identity, gemspec whitelist), not `test/` |
21
21
  | ✅ [F-11 Multi-layer test and benchmark suite](SPEC.md#testing-style) | [test/](test/) | Benchmarks live in [benchmark/](benchmark/) with the gate in `tasks/bench/`; the anchor baseline advances only by deliberate re-bless |
22
22
  | ✅ [F-12 Guest block reception and yield re-entry](docs/behavior/yield.md) | [lib/kobako/transport/yielder.rb](lib/kobako/transport/yielder.rb) | — |
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.12.0...kobako-runtime-v0.13.0) (2026-07-29)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **runtime:** mark the sets that grow, and say why the closed ones do not
9
+ * **spec:** separate what a name promises from what the wire promises
10
+ * **runtime:** give the engine contract names an implementer can write
11
+ * **sdk:** attribute an invocation without reading its payload
12
+ * **wire:** route every tier through the one envelope
13
+ * **outcome:** raise an unresolved entrypoint as its own error carrying the names it could have been
14
+ * **wire:** carry dispatch over the core envelope with an opaque payload
15
+
16
+ ### Features
17
+
18
+ * **outcome:** raise an unresolved entrypoint as its own error carrying the names it could have been ([05b4125](https://github.com/elct9620/kobako/commit/05b41257ca8d4bcb90d6759c6cc7b20582af0661))
19
+ * **runtime:** mark the sets that grow, and say why the closed ones do not ([1be2449](https://github.com/elct9620/kobako/commit/1be24492961e2c2a3a317f784cc2f20ae584dcbf))
20
+ * **wire:** add the core envelope, implemented independently on both peers ([e6c41d1](https://github.com/elct9620/kobako/commit/e6c41d179e5bf619d43d94160dd597c8ae3bc9bf))
21
+ * **wire:** carry dispatch over the core envelope with an opaque payload ([556104b](https://github.com/elct9620/kobako/commit/556104bf86fdf481b5368b70af83eb0add4b2708))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **sig:** declare the dispatch seam the shape the ext actually calls ([f0de465](https://github.com/elct9620/kobako/commit/f0de46500eda366d3edd7869edcc39d843f4b687))
27
+
28
+
29
+ ### Documentation
30
+
31
+ * **spec:** separate what a name promises from what the wire promises ([545bfbd](https://github.com/elct9620/kobako/commit/545bfbd59834bab6d564850d9adafa892cbae005))
32
+
33
+
34
+ ### Code Refactoring
35
+
36
+ * **runtime:** give the engine contract names an implementer can write ([0a41491](https://github.com/elct9620/kobako/commit/0a41491360cf17def8245c0758679e92572b990f))
37
+ * **sdk:** attribute an invocation without reading its payload ([70d22d0](https://github.com/elct9620/kobako/commit/70d22d01c79034d3d3011e95af5839d6eea654c8))
38
+ * **wire:** route every tier through the one envelope ([c5cd33a](https://github.com/elct9620/kobako/commit/c5cd33a5346f49857fa6e1f45c9cf9b9bea0ff77))
39
+
3
40
  ## [0.12.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.11.0...kobako-runtime-v0.12.0) (2026-07-24)
4
41
 
5
42
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  [package]
15
15
  name = "kobako-runtime"
16
- version = "0.12.0"
16
+ version = "0.13.0"
17
17
  edition = "2021"
18
18
  description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
19
19
  license = "Apache-2.0"
@@ -21,3 +21,10 @@ repository = "https://github.com/elct9620/kobako"
21
21
  readme = "README.md"
22
22
  keywords = ["wasm", "mruby", "sandbox", "wasi"]
23
23
  categories = ["wasm", "virtualization"]
24
+
25
+ # The fixed tier: the core envelope this contract routes and attributes
26
+ # through. The version pin rides the linked release group; the path keeps
27
+ # in-tree builds (and the Ruby gem, which ships both crates) resolving
28
+ # locally.
29
+ [dependencies]
30
+ kobako-transport = { version = "0.13.0", path = "../kobako-transport" }
@@ -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.12.0" # x-release-please-version
29
+ kobako-runtime = "0.13.0" # x-release-please-version
30
30
  ```
31
31
 
32
32
  ## License