kobako 0.4.0 → 0.5.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/Cargo.lock +1 -1
  5. data/README.md +0 -1
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +1 -1
  8. data/ext/kobako/src/lib.rs +4 -2
  9. data/ext/kobako/src/{wasm → runtime}/cache.rs +12 -16
  10. data/ext/kobako/src/runtime/capture.rs +91 -0
  11. data/ext/kobako/src/runtime/config.rs +26 -0
  12. data/ext/kobako/src/runtime/dispatch.rs +211 -0
  13. data/ext/kobako/src/runtime/exports.rs +51 -0
  14. data/ext/kobako/src/runtime/guest_mem.rs +228 -0
  15. data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +94 -86
  16. data/ext/kobako/src/runtime/trap.rs +134 -0
  17. data/ext/kobako/src/runtime.rs +782 -0
  18. data/ext/kobako/src/snapshot.rs +110 -0
  19. data/lib/kobako/capture.rb +11 -16
  20. data/lib/kobako/catalog/handles.rb +107 -0
  21. data/lib/kobako/catalog/namespaces.rb +99 -0
  22. data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
  23. data/lib/kobako/catalog.rb +18 -0
  24. data/lib/kobako/codec/decoder.rb +13 -5
  25. data/lib/kobako/codec/factory.rb +12 -12
  26. data/lib/kobako/codec/utils.rb +56 -59
  27. data/lib/kobako/codec.rb +6 -3
  28. data/lib/kobako/errors.rb +45 -28
  29. data/lib/kobako/fault.rb +40 -0
  30. data/lib/kobako/handle.rb +4 -6
  31. data/lib/kobako/namespace.rb +67 -0
  32. data/lib/kobako/outcome.rb +31 -35
  33. data/lib/kobako/runtime.rb +30 -0
  34. data/lib/kobako/sandbox.rb +83 -72
  35. data/lib/kobako/sandbox_options.rb +6 -9
  36. data/lib/kobako/snapshot.rb +40 -0
  37. data/lib/kobako/snippet/binary.rb +6 -7
  38. data/lib/kobako/snippet/source.rb +8 -8
  39. data/lib/kobako/snippet.rb +7 -9
  40. data/lib/kobako/transport/dispatcher.rb +195 -0
  41. data/lib/kobako/{rpc/wire_error.rb → transport/error.rb} +7 -6
  42. data/lib/kobako/transport/request.rb +78 -0
  43. data/lib/kobako/transport/response.rb +69 -0
  44. data/lib/kobako/transport/run.rb +141 -0
  45. data/lib/kobako/transport/yield.rb +91 -0
  46. data/lib/kobako/transport/yielder.rb +89 -0
  47. data/lib/kobako/transport.rb +24 -0
  48. data/lib/kobako/version.rb +1 -1
  49. data/lib/kobako.rb +4 -4
  50. data/release-please-config.json +24 -0
  51. data/sig/kobako/capture.rbs +0 -2
  52. data/sig/kobako/catalog/handles.rbs +19 -0
  53. data/sig/kobako/catalog/namespaces.rbs +17 -0
  54. data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
  55. data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
  56. data/sig/kobako/codec/decoder.rbs +2 -1
  57. data/sig/kobako/codec/factory.rbs +2 -2
  58. data/sig/kobako/codec/utils.rbs +5 -5
  59. data/sig/kobako/errors.rbs +7 -7
  60. data/sig/kobako/fault.rbs +19 -0
  61. data/sig/kobako/handle.rbs +2 -3
  62. data/sig/kobako/namespace.rbs +19 -0
  63. data/sig/kobako/outcome.rbs +2 -2
  64. data/sig/kobako/runtime.rbs +23 -0
  65. data/sig/kobako/sandbox.rbs +5 -8
  66. data/sig/kobako/snapshot.rbs +15 -0
  67. data/sig/kobako/transport/dispatcher.rbs +34 -0
  68. data/sig/kobako/transport/error.rbs +6 -0
  69. data/sig/kobako/transport/request.rbs +32 -0
  70. data/sig/kobako/transport/response.rbs +30 -0
  71. data/sig/kobako/transport/run.rbs +27 -0
  72. data/sig/kobako/transport/yield.rbs +34 -0
  73. data/sig/kobako/transport/yielder.rbs +21 -0
  74. data/sig/kobako/transport.rbs +4 -0
  75. metadata +48 -30
  76. data/ext/kobako/src/wasm/dispatch.rs +0 -162
  77. data/ext/kobako/src/wasm/instance.rs +0 -873
  78. data/ext/kobako/src/wasm.rs +0 -126
  79. data/lib/kobako/handle_table.rb +0 -119
  80. data/lib/kobako/invocation.rb +0 -143
  81. data/lib/kobako/rpc/dispatcher.rb +0 -171
  82. data/lib/kobako/rpc/envelope.rb +0 -118
  83. data/lib/kobako/rpc/fault.rb +0 -41
  84. data/lib/kobako/rpc/namespace.rb +0 -74
  85. data/lib/kobako/rpc/server.rb +0 -146
  86. data/lib/kobako/rpc.rb +0 -11
  87. data/lib/kobako/wasm.rb +0 -25
  88. data/sig/kobako/handle_table.rbs +0 -23
  89. data/sig/kobako/invocation.rbs +0 -25
  90. data/sig/kobako/rpc/dispatcher.rbs +0 -33
  91. data/sig/kobako/rpc/envelope.rbs +0 -51
  92. data/sig/kobako/rpc/fault.rbs +0 -20
  93. data/sig/kobako/rpc/namespace.rbs +0 -24
  94. data/sig/kobako/rpc/server.rbs +0 -31
  95. data/sig/kobako/rpc/wire_error.rbs +0 -6
  96. data/sig/kobako/wasm.rbs +0 -41
@@ -0,0 +1,228 @@
1
+ //! Caller-based guest linear-memory I/O shared by the host-import paths.
2
+ //!
3
+ //! Both directions of a host↔guest buffer handoff that run *inside* a wasm
4
+ //! callback frame go through here: writing the transport Response back
5
+ //! (`super::dispatch`) and shipping block-yield args into the guest
6
+ //! (`drive_yield`, below) performed the same `__kobako_alloc` +
7
+ //! bounds-check + `memory.write` dance with only the diagnostic strings
8
+ //! differing. The Store-based write path (`Runtime::write_envelope`) is a
9
+ //! separate beast — it holds the cached `Store`, not a `Caller` — and stays
10
+ //! in `runtime.rs`.
11
+
12
+ use wasmtime::{Caller, Extern, Memory};
13
+
14
+ use super::invocation::Invocation;
15
+
16
+ /// User-facing reason when a required guest export (the allocation or
17
+ /// block-yield hook) is absent or has the wrong signature — the loaded
18
+ /// `data/kobako.wasm` does not match the installed gem. Phrased in caller
19
+ /// vocabulary: the underlying hook symbol names are not actionable, and
20
+ /// the actionable fix is to rebuild the runtime.
21
+ const RUNTIME_INCOMPATIBLE: &str =
22
+ "the Sandbox runtime is incompatible; rebuild data/kobako.wasm against the installed version";
23
+
24
+ /// Resolve the guest's exported linear `memory`. The lookup shape (and its
25
+ /// diagnostic) is shared by every Caller-based path here — the write side
26
+ /// (`alloc_and_write`), the read side (`read`), and the yield round-trip
27
+ /// (`drive_yield`) — so the "no linear memory" reason lives in one place.
28
+ /// `read` maps the `Err` to its own `None` outcome via `.ok()`.
29
+ fn memory_export(caller: &mut Caller<'_, Invocation>) -> Result<Memory, &'static str> {
30
+ match caller.get_export("memory") {
31
+ Some(Extern::Memory(m)) => Ok(m),
32
+ _ => Err("the loaded Wasm module is not a Kobako-compatible runtime"),
33
+ }
34
+ }
35
+
36
+ /// Allocate `bytes.len()` bytes in guest memory via `__kobako_alloc` and
37
+ /// copy `bytes` in. Returns the guest pointer. Every failure path carries a
38
+ /// `&'static str` reason so the caller can surface a diagnostic rather than
39
+ /// a silent fault.
40
+ pub(super) fn alloc_and_write(
41
+ caller: &mut Caller<'_, Invocation>,
42
+ bytes: &[u8],
43
+ ) -> Result<u32, &'static str> {
44
+ let alloc = match caller.get_export("__kobako_alloc") {
45
+ Some(Extern::Func(f)) => f
46
+ .typed::<i32, i32>(&*caller)
47
+ .map_err(|_| RUNTIME_INCOMPATIBLE)?,
48
+ _ => return Err(RUNTIME_INCOMPATIBLE),
49
+ };
50
+ let len = checked_payload_len(bytes.len())?;
51
+ let ptr = alloc
52
+ .call(&mut *caller, len)
53
+ .map_err(|_| "the Sandbox trapped while allocating memory for the request")?;
54
+ if ptr == 0 {
55
+ return Err("the Sandbox ran out of memory while preparing the request");
56
+ }
57
+
58
+ let mem = memory_export(caller)?;
59
+ mem.write(&mut *caller, ptr as usize, bytes)
60
+ .map_err(|_| "could not write the request into the Sandbox's memory")?;
61
+ Ok(ptr as u32)
62
+ }
63
+
64
+ /// Copy `[ptr, ptr + len)` out of the guest's linear memory as seen from
65
+ /// `caller`. Each failure carries a `&'static str` reason — matching the
66
+ /// other Caller-based ops here — so the caller surfaces a specific
67
+ /// diagnostic instead of a lumped one; a guest-claimed length past the
68
+ /// 16 MiB cap is a wire violation that names the cap (the caller walks
69
+ /// the trap path on any `Err`).
70
+ pub(super) fn read(
71
+ caller: &mut Caller<'_, Invocation>,
72
+ ptr: i32,
73
+ len: i32,
74
+ ) -> Result<Vec<u8>, &'static str> {
75
+ let len = usize::try_from(len).map_err(|_| "the Sandbox produced a negative request length")?;
76
+ if len > MAX_DISPATCH_PAYLOAD {
77
+ return Err("request payload exceeds the 16 MiB limit");
78
+ }
79
+ let mem = memory_export(caller)?;
80
+ let data = mem.data(&caller);
81
+ let start =
82
+ usize::try_from(ptr).map_err(|_| "the Sandbox produced a negative request pointer")?;
83
+ let end = start
84
+ .checked_add(len)
85
+ .ok_or("the Sandbox produced an out-of-range request")?;
86
+ data.get(start..end)
87
+ .map(|s| s.to_vec())
88
+ .ok_or("the Sandbox produced an out-of-bounds request")
89
+ }
90
+
91
+ /// Single-dispatch payload cap: 16 MiB in either direction
92
+ /// (SPEC.md § Wire Codec; docs/wire-codec.md § ABI). A host↔guest
93
+ /// transfer larger than this is a wire violation — the Host Gem walks
94
+ /// the trap path rather than allocate or copy the buffer. Held as a
95
+ /// constant for now; a future SPEC anchor may let the Host App raise it.
96
+ pub(super) const MAX_DISPATCH_PAYLOAD: usize = 16 * 1024 * 1024;
97
+
98
+ /// Validate a payload length against `MAX_DISPATCH_PAYLOAD` and narrow it
99
+ /// to `i32` — the signed wasm ABI width for the guest buffer parameters.
100
+ /// Every host *write* boundary (`alloc_and_write`, `drive_yield`,
101
+ /// `Runtime::write_envelope`) routes its length through here so the
102
+ /// wire-violation reason is uniform; the *read* boundaries compare
103
+ /// against `MAX_DISPATCH_PAYLOAD` directly.
104
+ pub(super) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
105
+ if len > MAX_DISPATCH_PAYLOAD {
106
+ return Err("payload exceeds the 16 MiB limit");
107
+ }
108
+ // The cap above sits below `i32::MAX`, so this conversion cannot wrap.
109
+ i32::try_from(len).map_err(|_| "payload exceeds the 16 MiB limit")
110
+ }
111
+
112
+ /// Compute the half-open range `[ptr, ptr + len)` for a guest linear-memory
113
+ /// copy, validating that the arithmetic does not overflow and the range
114
+ /// fits inside `mem_size`. Shared by `Runtime::write_envelope` (write side)
115
+ /// and `Runtime::fetch_outcome_bytes` (read side).
116
+ pub(super) fn guest_buffer_range(
117
+ ptr: usize,
118
+ len: usize,
119
+ mem_size: usize,
120
+ ) -> Result<core::ops::Range<usize>, &'static str> {
121
+ let end = ptr.checked_add(len).ok_or("ptr + len overflow")?;
122
+ if end > mem_size {
123
+ return Err("range exceeds Sandbox memory size");
124
+ }
125
+ Ok(ptr..end)
126
+ }
127
+
128
+ /// Unpack the `(ptr, len)` u64 returned by `__kobako_take_outcome`:
129
+ /// high 32 bits = ptr, low 32 bits = len. Mirrors the guest-side
130
+ /// `crate::abi::unpack_u64` in `wasm/kobako-wasm/src/abi.rs`.
131
+ pub(super) fn unpack_outcome_packed(packed: u64) -> (usize, usize) {
132
+ let ptr = (packed >> 32) as u32 as usize;
133
+ let len = packed as u32 as usize;
134
+ (ptr, len)
135
+ }
136
+
137
+ /// Allocate `args.len()` bytes in guest memory, copy the args payload in,
138
+ /// call `__kobako_yield_to_block(ptr, len)`, then read the response slice
139
+ /// the guest produced and return it. Mirrors `dispatch::write_response`'s
140
+ /// allocator dance but in the opposite direction — the host is the
141
+ /// *initiator* of this round-trip, not the responder.
142
+ pub(super) fn drive_yield(
143
+ caller: &mut Caller<'_, Invocation>,
144
+ args: &[u8],
145
+ ) -> Result<Vec<u8>, &'static str> {
146
+ let len_i32 = checked_payload_len(args.len())?;
147
+ let req_ptr = alloc_and_write(caller, args)? as i32;
148
+
149
+ let yield_fn = match caller.get_export("__kobako_yield_to_block") {
150
+ Some(Extern::Func(f)) => f
151
+ .typed::<(i32, i32), u64>(&*caller)
152
+ .map_err(|_| RUNTIME_INCOMPATIBLE)?,
153
+ _ => return Err(RUNTIME_INCOMPATIBLE),
154
+ };
155
+ let packed = yield_fn
156
+ .call(&mut *caller, (req_ptr, len_i32))
157
+ .map_err(|_| "the Sandbox trapped while invoking a block")?;
158
+ let (resp_ptr, resp_len) = unpack_outcome_packed(packed);
159
+ if resp_len == 0 {
160
+ return Err("the Sandbox returned an empty block result");
161
+ }
162
+ if resp_len > MAX_DISPATCH_PAYLOAD {
163
+ return Err("block result payload exceeds the 16 MiB limit");
164
+ }
165
+
166
+ let mem = memory_export(caller)?;
167
+ let data = mem.data(&caller);
168
+ let range = guest_buffer_range(resp_ptr, resp_len, data.len())
169
+ .map_err(|_| "the Sandbox returned an out-of-bounds block result")?;
170
+ Ok(data[range].to_vec())
171
+ }
172
+
173
+ #[cfg(test)]
174
+ mod tests {
175
+ use super::{
176
+ checked_payload_len, guest_buffer_range, unpack_outcome_packed, MAX_DISPATCH_PAYLOAD,
177
+ };
178
+
179
+ #[test]
180
+ fn checked_payload_len_accepts_zero_and_the_cap() {
181
+ assert_eq!(checked_payload_len(0), Ok(0));
182
+ assert_eq!(
183
+ checked_payload_len(MAX_DISPATCH_PAYLOAD),
184
+ Ok(MAX_DISPATCH_PAYLOAD as i32)
185
+ );
186
+ }
187
+
188
+ #[test]
189
+ fn checked_payload_len_rejects_past_the_cap() {
190
+ assert!(checked_payload_len(MAX_DISPATCH_PAYLOAD + 1).is_err());
191
+ assert!(checked_payload_len(usize::MAX).is_err());
192
+ }
193
+
194
+ #[test]
195
+ fn guest_buffer_range_returns_half_open_range() {
196
+ assert_eq!(guest_buffer_range(10, 5, 100), Ok(10..15));
197
+ }
198
+
199
+ #[test]
200
+ fn guest_buffer_range_accepts_zero_length_at_any_in_bounds_ptr() {
201
+ assert_eq!(guest_buffer_range(0, 0, 0), Ok(0..0));
202
+ assert_eq!(guest_buffer_range(42, 0, 100), Ok(42..42));
203
+ }
204
+
205
+ #[test]
206
+ fn guest_buffer_range_rejects_ptr_plus_len_overflow() {
207
+ assert!(guest_buffer_range(usize::MAX, 1, usize::MAX).is_err());
208
+ }
209
+
210
+ #[test]
211
+ fn guest_buffer_range_rejects_end_past_memory() {
212
+ assert!(guest_buffer_range(10, 100, 50).is_err());
213
+ assert_eq!(guest_buffer_range(0, 50, 50), Ok(0..50));
214
+ }
215
+
216
+ #[test]
217
+ fn unpack_outcome_packed_extracts_high_ptr_low_len() {
218
+ assert_eq!(
219
+ unpack_outcome_packed(0xAABB_CCDD_1122_3344),
220
+ (0xAABB_CCDD, 0x1122_3344)
221
+ );
222
+ }
223
+
224
+ #[test]
225
+ fn unpack_outcome_packed_zero_decodes_to_zero_pair() {
226
+ assert_eq!(unpack_outcome_packed(0), (0, 0));
227
+ }
228
+ }