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
@@ -1,162 +0,0 @@
1
- //! Host-side dispatch for the `__kobako_dispatch` import.
2
- //!
3
- //! When the guest invokes the wasm import declared in
4
- //! `wasm/kobako-wasm/src/abi.rs`, wasmtime calls back into the host
5
- //! through the closure built in [`super::instance::Instance::build`].
6
- //! That closure delegates here. The dispatcher (docs/behavior.md B-12 / B-13):
7
- //!
8
- //! 1. Reads the Request bytes from guest linear memory.
9
- //! 2. Hands them to the Ruby-side `Kobako::RPC::Server` and recovers
10
- //! Response bytes.
11
- //! 3. Allocates a guest buffer via `__kobako_alloc(len)` invoked
12
- //! through `Caller::get_export`.
13
- //! 4. Writes the Response bytes into the guest buffer.
14
- //! 5. Returns packed `(ptr<<32)|len` for the guest to decode.
15
- //!
16
- //! Returns 0 on any step failure. `Kobako::Sandbox` always installs a
17
- //! Server before invoking the guest, so reaching the dispatcher with
18
- //! no Server bound is itself a wire-layer fault; the guest maps a 0
19
- //! return to a trap. Failures during normal dispatch surface as
20
- //! Response.err envelopes from the Server itself — they never reach
21
- //! this 0-return path.
22
- //!
23
- //! ## Why this module writes to `stderr`
24
- //!
25
- //! This file is the one place in `ext/` that deliberately prints
26
- //! through `eprintln!`. The host normally surfaces faults by
27
- //! raising a `MagnusError` back into Ruby; the dispatcher contract
28
- //! is the exception — it must return a packed `i64` to the guest
29
- //! and cannot raise, so a 0 return is the only signal the wasm side
30
- //! receives. The guest collapses every 0 into the same trap, so the
31
- //! Ruby host has no way to attribute the failure to a specific
32
- //! step (missing `memory` export vs. no Server bound vs. Server
33
- //! raised vs. `__kobako_alloc` returned 0 vs. `memory.write`
34
- //! rejected).
35
- //!
36
- //! [`handle`] writes a single `[kobako-dispatch] <reason>` line to
37
- //! `stderr` on each failure path so operators have a breadcrumb to
38
- //! correlate the trap with the actual cause. The line is emitted in
39
- //! both debug and release builds on purpose: dispatcher failures
40
- //! are wire-layer faults rather than expected error paths
41
- //! (`Kobako::Sandbox` always installs a Server, the Server is
42
- //! contracted never to raise, etc.), so the "release-build noise"
43
- //! cost is bounded — under normal operation the line is never
44
- //! written. Operators that need to silence the channel can redirect
45
- //! the host process's stderr, but the kobako convention is "ext
46
- //! never logs" plus this single, named exception.
47
-
48
- use magnus::value::{Opaque, ReprValue};
49
- use magnus::{Error as MagnusError, RString, Ruby, Value};
50
- use wasmtime::{Caller, Extern};
51
-
52
- use super::host_state::HostState;
53
-
54
- /// Drive a single `__kobako_dispatch` invocation end-to-end. Entry point
55
- /// from the wasmtime closure built in [`super::instance::Instance::build`].
56
- ///
57
- /// Returns the packed `(ptr<<32)|len` u64 on success, 0 on any
58
- /// wire-layer fault. Failure paths log a `[kobako-dispatch]` line to
59
- /// `stderr` so operators have a breadcrumb when the guest sees a 0
60
- /// return and traps; before this every failure was silent. The Server
61
- /// itself is contracted never to raise (it folds Service exceptions
62
- /// into Response.err envelopes), so reaching the failure path is
63
- /// always a wiring bug or wire-layer fault rather than an expected
64
- /// path.
65
- pub(crate) fn handle(caller: &mut Caller<'_, HostState>, req_ptr: i32, req_len: i32) -> i64 {
66
- match try_handle(caller, req_ptr, req_len) {
67
- Ok(packed) => packed,
68
- Err(reason) => {
69
- eprintln!("[kobako-dispatch] {}", reason);
70
- 0
71
- }
72
- }
73
- }
74
-
75
- /// Result-returning core of [`handle`]. Pulled out so each early
76
- /// failure path carries a diagnostic string instead of an opaque 0.
77
- fn try_handle(
78
- caller: &mut Caller<'_, HostState>,
79
- req_ptr: i32,
80
- req_len: i32,
81
- ) -> Result<i64, &'static str> {
82
- let req_bytes = read_caller_memory(caller, req_ptr, req_len).ok_or(
83
- "Sandbox runtime does not export linear memory, or RPC request slice falls outside it",
84
- )?;
85
-
86
- // `Kobako::Sandbox` always installs an RPC server before invoking
87
- // the runtime, so reaching this branch indicates a misuse rather
88
- // than a normal control path.
89
- let server = caller
90
- .data()
91
- .server()
92
- .ok_or("RPC dispatched outside an active Sandbox#run — internal wiring bug")?;
93
-
94
- let resp_bytes = invoke_server(server, &req_bytes).map_err(|_| {
95
- "RPC server raised an exception instead of returning a fault — please report this as a kobako bug"
96
- })?;
97
-
98
- write_response(caller, &resp_bytes)
99
- }
100
-
101
- /// Call the Ruby Server's `#dispatch(request_bytes)` method and return
102
- /// the encoded Response bytes. Errors here mean the Server itself
103
- /// failed (it is contracted never to raise — see
104
- /// `Kobako::RPC::Server#dispatch`), which we treat as a wire-layer fault.
105
- fn invoke_server(server: Opaque<Value>, req_bytes: &[u8]) -> Result<Vec<u8>, MagnusError> {
106
- // The wasmtime callback runs on the same Ruby thread that called the
107
- // active Sandbox invocation (#eval or #run) — the invariant SPEC
108
- // Implementation Standards Architecture pins for the host gem — so
109
- // `Ruby::get()` is always available here. Panicking with `expect`
110
- // localises the violation rather than letting a nonsense error
111
- // propagate.
112
- let ruby = Ruby::get().expect("Ruby handle unavailable in __kobako_dispatch");
113
- let server_value: Value = ruby.get_inner(server);
114
- let req_str = ruby.str_from_slice(req_bytes);
115
- let resp: RString = server_value.funcall("dispatch", (req_str,))?;
116
- Ok(super::rstring_to_vec(resp))
117
- }
118
-
119
- /// Allocate a guest-side buffer through `__kobako_alloc` and copy the
120
- /// response bytes into it. Returns the packed `(ptr<<32)|len` u64.
121
- /// Each failure path carries a `&'static str` reason so the dispatcher
122
- /// wrapper can surface a useful diagnostic rather than a silent 0.
123
- fn write_response(caller: &mut Caller<'_, HostState>, bytes: &[u8]) -> Result<i64, &'static str> {
124
- let alloc = match caller.get_export("__kobako_alloc") {
125
- Some(Extern::Func(f)) => f
126
- .typed::<i32, i32>(&*caller)
127
- .map_err(|_| "Sandbox runtime's allocation hook has the wrong signature")?,
128
- _ => return Err("Sandbox runtime is missing the allocation hook"),
129
- };
130
- let len_i32 = i32::try_from(bytes.len()).map_err(|_| "RPC response exceeds 2 GiB")?;
131
- let ptr = alloc
132
- .call(&mut *caller, len_i32)
133
- .map_err(|_| "Sandbox allocation trapped while preparing the RPC response")?;
134
- if ptr == 0 {
135
- return Err("Sandbox is out of memory while preparing the RPC response");
136
- }
137
-
138
- let mem = match caller.get_export("memory") {
139
- Some(Extern::Memory(m)) => m,
140
- _ => return Err("Sandbox runtime does not export linear memory"),
141
- };
142
- mem.write(&mut *caller, ptr as usize, bytes)
143
- .map_err(|_| "could not write the RPC response into Sandbox memory (range invalid)")?;
144
-
145
- let ptr_u32 = ptr as u32;
146
- let len_u32 = bytes.len() as u32;
147
- Ok(((ptr_u32 as i64) << 32) | (len_u32 as i64))
148
- }
149
-
150
- /// Copy `[ptr, ptr+len)` out of the guest's linear memory as seen from
151
- /// `caller`. Returns `None` when `memory` is not exported or the slice
152
- /// falls outside the live memory range.
153
- fn read_caller_memory(caller: &mut Caller<'_, HostState>, ptr: i32, len: i32) -> Option<Vec<u8>> {
154
- let mem = match caller.get_export("memory") {
155
- Some(Extern::Memory(m)) => m,
156
- _ => return None,
157
- };
158
- let data = mem.data(&caller);
159
- let start = ptr as usize;
160
- let end = start.checked_add(len as usize)?;
161
- data.get(start..end).map(|s| s.to_vec())
162
- }