kobako 0.12.1 → 0.13.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +22 -0
  4. data/Cargo.lock +138 -136
  5. data/Cargo.toml +6 -2
  6. data/README.md +3 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +16 -0
  8. data/crates/kobako-runtime/Cargo.toml +23 -0
  9. data/crates/kobako-runtime/README.md +34 -0
  10. data/crates/kobako-runtime/src/dispatch.rs +22 -0
  11. data/crates/kobako-runtime/src/error.rs +64 -0
  12. data/crates/kobako-runtime/src/lib.rs +18 -0
  13. data/crates/kobako-runtime/src/profile.rs +35 -0
  14. data/crates/kobako-runtime/src/runtime.rs +57 -0
  15. data/crates/kobako-runtime/src/snapshot.rs +46 -0
  16. data/crates/kobako-runtime/src/yielder.rs +22 -0
  17. data/crates/kobako-wasmtime/CHANGELOG.md +16 -0
  18. data/crates/kobako-wasmtime/Cargo.toml +62 -0
  19. data/crates/kobako-wasmtime/README.md +32 -0
  20. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/ambient.rs +8 -5
  21. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/cache.rs +30 -41
  22. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/capture.rs +2 -2
  23. data/crates/kobako-wasmtime/src/config.rs +33 -0
  24. data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
  25. data/crates/kobako-wasmtime/src/driver.rs +297 -0
  26. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
  27. data/crates/kobako-wasmtime/src/frames.rs +276 -0
  28. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/guest_mem.rs +38 -15
  29. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/instance_pre.rs +13 -21
  30. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/invocation.rs +54 -49
  31. data/crates/kobako-wasmtime/src/lib.rs +47 -0
  32. data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/trap.rs +29 -35
  33. data/data/kobako.wasm +0 -0
  34. data/ext/kobako/Cargo.toml +9 -32
  35. data/ext/kobako/src/lib.rs +0 -2
  36. data/ext/kobako/src/runtime/bridge.rs +150 -0
  37. data/ext/kobako/src/runtime/errors.rs +45 -13
  38. data/ext/kobako/src/runtime.rs +246 -420
  39. data/lib/kobako/catalog/handles.rb +3 -3
  40. data/lib/kobako/catalog/namespaces.rb +4 -0
  41. data/lib/kobako/catalog/snippets.rb +4 -0
  42. data/lib/kobako/codec/encoder.rb +5 -1
  43. data/lib/kobako/codec/factory.rb +41 -13
  44. data/lib/kobako/codec/handle_walk.rb +4 -0
  45. data/lib/kobako/codec.rb +1 -1
  46. data/lib/kobako/errors.rb +18 -16
  47. data/lib/kobako/sandbox.rb +71 -39
  48. data/lib/kobako/sandbox_options.rb +71 -13
  49. data/lib/kobako/transport/dispatcher.rb +2 -2
  50. data/lib/kobako/transport/response.rb +14 -14
  51. data/lib/kobako/transport/run.rb +2 -6
  52. data/lib/kobako/transport/yield.rb +1 -1
  53. data/lib/kobako/transport/yielder.rb +2 -2
  54. data/lib/kobako/version.rb +1 -1
  55. data/lib/kobako.rb +0 -1
  56. data/release-please-config.json +78 -3
  57. data/sig/kobako/codec/factory.rbs +3 -0
  58. data/sig/kobako/errors.rbs +7 -14
  59. data/sig/kobako/runtime.rbs +16 -6
  60. data/sig/kobako/sandbox.rbs +11 -7
  61. data/sig/kobako/sandbox_options.rbs +15 -4
  62. data/sig/kobako/transport/dispatcher.rbs +1 -1
  63. data/sig/kobako/transport/run.rbs +2 -2
  64. data/sig/kobako/transport/yielder.rbs +2 -2
  65. data/sig/kobako/transport.rbs +8 -0
  66. metadata +28 -15
  67. data/ext/kobako/src/runtime/config.rs +0 -25
  68. data/ext/kobako/src/runtime/dispatch.rs +0 -211
  69. data/ext/kobako/src/runtime/frames.rs +0 -188
  70. data/ext/kobako/src/snapshot.rs +0 -110
  71. data/lib/kobako/snapshot.rb +0 -38
  72. data/sig/kobako/snapshot.rbs +0 -15
@@ -1,188 +0,0 @@
1
- //! Per-invocation byte-shuttle between Ruby and guest linear memory: it
2
- //! resolves the required `memory` / ABI-export handles, writes the `#run`
3
- //! envelope into a freshly allocated guest buffer, builds the stdin frame
4
- //! stream plus stdout / stderr capture pipes for the WASI context, and
5
- //! reads the OUTCOME_BUFFER back out. The ext owns no wire codec — these
6
- //! helpers move raw bytes; Ruby decodes them.
7
-
8
- use magnus::{Error as MagnusError, RString, Ruby};
9
- use wasmtime::{AsContextMut, Memory, Store as WtStore, TypedFunc};
10
- use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
11
- use wasmtime_wasi::WasiCtxBuilder;
12
-
13
- use super::config::Config;
14
- use super::exports::Exports;
15
- use super::invocation::Invocation;
16
- use super::rstring_to_vec;
17
- use super::{ambient, capture, errors, guest_mem};
18
-
19
- /// Return the resolved `memory` export handle, or raise
20
- /// `Kobako::TrapError` when the loaded module exports no linear
21
- /// memory — the "not a Kobako-shaped runtime" failure mode
22
- /// (`SANDBOX_RUNTIME_NOT_KOBAKO`).
23
- fn require_memory(ruby: &Ruby, exports: &Exports) -> Result<Memory, MagnusError> {
24
- exports
25
- .memory
26
- .ok_or_else(|| errors::trap_err(ruby, SANDBOX_RUNTIME_NOT_KOBAKO))
27
- }
28
-
29
- /// Allocate a `len`-byte buffer in guest linear memory via
30
- /// `__kobako_alloc`, copy `envelope` into it, and return `(ptr, len)`
31
- /// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
32
- /// Raises `Kobako::TrapError` when the allocation hook is missing or
33
- /// itself traps, and `Kobako::SandboxError` when the hook runs but
34
- /// cannot reserve the buffer (`__kobako_alloc` returns 0) — an
35
- /// intact runtime, not an engine fault.
36
- pub(crate) fn write_envelope(
37
- ruby: &Ruby,
38
- store: &mut WtStore<Invocation>,
39
- exports: &Exports,
40
- envelope: RString,
41
- ) -> Result<(i32, i32), MagnusError> {
42
- let bytes = rstring_to_vec(envelope);
43
- let len_i32 =
44
- guest_mem::checked_payload_len(bytes.len()).map_err(|msg| errors::trap_err(ruby, msg))?;
45
-
46
- let alloc = require_export(ruby, exports.alloc.as_ref())?;
47
- let memory = require_memory(ruby, exports)?;
48
-
49
- let ptr = alloc
50
- .call(store.as_context_mut(), bytes.len() as u32)
51
- .map_err(|e| errors::trap_err(ruby, format!("failed to allocate input buffer: {}", e)))?;
52
- if ptr == 0 {
53
- return Err(errors::sandbox_err(
54
- ruby,
55
- "could not allocate input buffer (out of memory)",
56
- ));
57
- }
58
- let data = memory.data_mut(store.as_context_mut());
59
- let range = guest_mem::guest_buffer_range(ptr as usize, bytes.len(), data.len())
60
- .map_err(|msg| errors::trap_err(ruby, msg))?;
61
- data[range].copy_from_slice(&bytes);
62
-
63
- Ok((ptr as i32, len_i32))
64
- }
65
-
66
- /// Build the per-invocation WASI context with stdin carrying every frame
67
- /// in `frames` (each prefixed by its 4-byte big-endian u32 length —
68
- /// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
69
- /// pipes, and install it on the invocation's Store. `#eval` passes three
70
- /// frames (preamble, source, snippets), `#run` passes two (preamble,
71
- /// snippets — the invocation envelope arrives via linear memory
72
- /// instead). Each output pipe is sized at `cap + 1` so
73
- /// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
74
- /// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
75
- /// rely on `memory_limit` for the real ceiling.
76
- /// Raises `Kobako::TrapError` when any frame exceeds the 16 MiB cap that
77
- /// keeps its `u32` length prefix from wrapping.
78
- pub(crate) fn install_wasi_frames(
79
- store: &mut WtStore<Invocation>,
80
- config: &Config,
81
- frames: &[Vec<u8>],
82
- ) -> Result<(), MagnusError> {
83
- let ruby = Ruby::get().expect("Ruby thread");
84
- // Every frame carries the same 16 MiB cap as the `#run` envelope
85
- // (`write_envelope`): the length prefix is a `u32`, so a frame past
86
- // the cap would silently wrap and corrupt the stdin frame stream.
87
- for frame in frames {
88
- guest_mem::checked_payload_len(frame.len()).map_err(|msg| errors::trap_err(&ruby, msg))?;
89
- }
90
-
91
- let total: usize = frames.iter().map(|f| 4 + f.len()).sum();
92
- let mut stdin_content: Vec<u8> = Vec::with_capacity(total);
93
- for frame in frames {
94
- stdin_content.extend_from_slice(&(frame.len() as u32).to_be_bytes());
95
- stdin_content.extend_from_slice(frame);
96
- }
97
-
98
- let stdin_pipe = MemoryInputPipe::new(stdin_content);
99
- let stdout_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stdout_limit_bytes));
100
- let stderr_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stderr_limit_bytes));
101
-
102
- let mut builder = WasiCtxBuilder::new();
103
- builder.stdin(stdin_pipe);
104
- builder.stdout(stdout_pipe.clone());
105
- builder.stderr(stderr_pipe.clone());
106
- // Deny the preview1 ambient-authority imports the guest never legitimately
107
- // reaches but the WASI layer would otherwise grant (see `ambient`).
108
- builder.wall_clock(ambient::FrozenWallClock);
109
- builder.monotonic_clock(ambient::FrozenMonotonicClock);
110
- builder.secure_random(ambient::deterministic_rng());
111
- let wasi = builder.build_p1();
112
-
113
- store
114
- .data_mut()
115
- .install_wasi(wasi, stdout_pipe, stderr_pipe);
116
- Ok(())
117
- }
118
-
119
- /// Invoke `__kobako_take_outcome`, decode the packed `(ptr<<32)|len`
120
- /// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Raises
121
- /// `Kobako::TrapError` when the export is missing, `len` exceeds the
122
- /// 16 MiB single-dispatch cap, the `ptr`/`len` arithmetic overflows,
123
- /// the slice falls outside live memory, or the `memory` export itself
124
- /// is absent.
125
- pub(crate) fn fetch_outcome_bytes(
126
- ruby: &Ruby,
127
- store: &mut WtStore<Invocation>,
128
- exports: &Exports,
129
- ) -> Result<Vec<u8>, MagnusError> {
130
- let take = require_export(ruby, exports.take_outcome.as_ref())?;
131
- let mem = require_memory(ruby, exports)?;
132
-
133
- let packed = take
134
- .call(store.as_context_mut(), ())
135
- .map_err(|e| errors::trap_err(ruby, format!("failed to read the Sandbox result: {}", e)))?;
136
- let (ptr, len) = guest_mem::unpack_outcome_packed(packed);
137
- if len > guest_mem::MAX_DISPATCH_PAYLOAD {
138
- return Err(errors::trap_err(
139
- ruby,
140
- "result payload exceeds the 16 MiB limit",
141
- ));
142
- }
143
-
144
- let data = mem.data(store.as_context_mut());
145
- let range = guest_mem::guest_buffer_range(ptr, len, data.len()).map_err(|msg| {
146
- errors::trap_err(
147
- ruby,
148
- format!("the Sandbox result is out of bounds: {}", msg),
149
- )
150
- })?;
151
- Ok(data[range].to_vec())
152
- }
153
-
154
- /// User-facing message for the "Sandbox runtime is missing one of the
155
- /// internal Kobako hooks" failure mode. Phrased in caller vocabulary —
156
- /// the underlying ABI symbol names (`__kobako_alloc`, `__kobako_eval`,
157
- /// `__kobako_take_outcome`) are not actionable to callers, and the
158
- /// gem itself raises this error so a self-reference like "matches the
159
- /// kobako gem version" reads as third-person. The actionable
160
- /// diagnosis is "your data/kobako.wasm is out of sync; rebuild it".
161
- const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required hooks; \
162
- rebuild data/kobako.wasm against the installed version";
163
-
164
- /// User-facing message for the "the loaded Wasm module is not a
165
- /// Kobako-shaped runtime at all" failure mode (no linear memory
166
- /// export). Same phrasing philosophy as
167
- /// `SANDBOX_RUNTIME_MISSING_HOOKS`.
168
- const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
169
- "the loaded Wasm module is not a Kobako-compatible runtime";
170
-
171
- /// Return the resolved `TypedFunc` for an ABI export, or raise
172
- /// `Kobako::TrapError` when the option is `None`. Both run-path
173
- /// methods (`#eval`, `#run`) plus the `build_snapshot` readout that
174
- /// drains `OUTCOME_BUFFER` share the same "missing export → Ruby
175
- /// error" boilerplate; this helper collapses those sites onto one
176
- /// safe entry. The user-facing message is intentionally export-
177
- /// agnostic (see `SANDBOX_RUNTIME_MISSING_HOOKS`) — the ABI symbol
178
- /// name is not actionable to callers, so it is not threaded in.
179
- pub(crate) fn require_export<'a, Params, Results>(
180
- ruby: &Ruby,
181
- export: Option<&'a TypedFunc<Params, Results>>,
182
- ) -> Result<&'a TypedFunc<Params, Results>, MagnusError>
183
- where
184
- Params: wasmtime::WasmParams,
185
- Results: wasmtime::WasmResults,
186
- {
187
- export.ok_or_else(|| errors::trap_err(ruby, SANDBOX_RUNTIME_MISSING_HOOKS))
188
- }
@@ -1,110 +0,0 @@
1
- //! `Kobako::Snapshot` — per-invocation observable bundle.
2
- //!
3
- //! Every successful `Kobako::Runtime#eval` / `#run` returns one of these.
4
- //! It carries every observable the host needs to surface after a guest
5
- //! invocation: the OUTCOME_BUFFER bytes (`return_bytes`), the captured
6
- //! stdout / stderr byte slices with their truncation flags, and
7
- //! the wall-clock + memory-peak figures from `Kobako::Usage`.
8
- //!
9
- //! Ruby callers see the seven raw readers registered below; the helper
10
- //! methods that pack them into `Kobako::Capture` / `Kobako::Usage`
11
- //! (`Kobako::Snapshot#stdout` / `#stderr` / `#usage`) live in
12
- //! `lib/kobako/snapshot.rb`. The split keeps the ext side a pure value
13
- //! carrier and lets Ruby own the convenience surface.
14
-
15
- use std::cell::Cell;
16
- use std::time::Duration;
17
-
18
- use magnus::{method, prelude::*, Error as MagnusError, RModule, RString, Ruby};
19
-
20
- /// Per-invocation snapshot value. Magnus wraps it so a single ext call
21
- /// from `Runtime::eval` / `Runtime::run` returns the whole bundle —
22
- /// the Sandbox layer can decompose it without round-tripping into ext
23
- /// again. All fields are private; the seven public methods registered
24
- /// in `init` read them out one by one. The wall-clock duration is
25
- /// held as a `Cell<Duration>` only because magnus' `#[magnus::wrap]`
26
- /// macro requires interior mutability — every field is set once at
27
- /// construction time and never mutated again.
28
- #[magnus::wrap(class = "Kobako::Snapshot", free_immediately, size)]
29
- pub(crate) struct Snapshot {
30
- return_bytes: Vec<u8>,
31
- stdout_bytes: Vec<u8>,
32
- stdout_truncated: bool,
33
- stderr_bytes: Vec<u8>,
34
- stderr_truncated: bool,
35
- wall_time: Cell<Duration>,
36
- memory_peak: usize,
37
- }
38
-
39
- impl Snapshot {
40
- /// Construct a fresh Snapshot from the per-invocation data the
41
- /// Runtime has just collected. Called from
42
- /// `crate::runtime::Runtime::build_snapshot` once the
43
- /// guest export has returned, the OUTCOME_BUFFER has been drained,
44
- /// and the capture pipes have been clipped to their caps.
45
- pub(crate) fn new(
46
- return_bytes: Vec<u8>,
47
- stdout_bytes: Vec<u8>,
48
- stdout_truncated: bool,
49
- stderr_bytes: Vec<u8>,
50
- stderr_truncated: bool,
51
- wall_time: Duration,
52
- memory_peak: usize,
53
- ) -> Self {
54
- Self {
55
- return_bytes,
56
- stdout_bytes,
57
- stdout_truncated,
58
- stderr_bytes,
59
- stderr_truncated,
60
- wall_time: Cell::new(wall_time),
61
- memory_peak,
62
- }
63
- }
64
-
65
- fn return_bytes(&self) -> RString {
66
- let ruby = Ruby::get().expect("Ruby thread");
67
- ruby.str_from_slice(&self.return_bytes)
68
- }
69
-
70
- fn stdout_bytes(&self) -> RString {
71
- let ruby = Ruby::get().expect("Ruby thread");
72
- ruby.str_from_slice(&self.stdout_bytes)
73
- }
74
-
75
- fn stdout_truncated(&self) -> bool {
76
- self.stdout_truncated
77
- }
78
-
79
- fn stderr_bytes(&self) -> RString {
80
- let ruby = Ruby::get().expect("Ruby thread");
81
- ruby.str_from_slice(&self.stderr_bytes)
82
- }
83
-
84
- fn stderr_truncated(&self) -> bool {
85
- self.stderr_truncated
86
- }
87
-
88
- fn wall_time(&self) -> f64 {
89
- self.wall_time.get().as_secs_f64()
90
- }
91
-
92
- fn memory_peak(&self) -> usize {
93
- self.memory_peak
94
- }
95
- }
96
-
97
- /// Register `Kobako::Snapshot` plus its seven raw readers under the
98
- /// `Kobako` module. Called from `crate::init` after `Kobako::Runtime`
99
- /// is registered so the magnus wrap macro can resolve the class name.
100
- pub(crate) fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
101
- let snapshot = kobako.define_class("Snapshot", ruby.class_object())?;
102
- snapshot.define_method("return_bytes", method!(Snapshot::return_bytes, 0))?;
103
- snapshot.define_method("stdout_bytes", method!(Snapshot::stdout_bytes, 0))?;
104
- snapshot.define_method("stdout_truncated", method!(Snapshot::stdout_truncated, 0))?;
105
- snapshot.define_method("stderr_bytes", method!(Snapshot::stderr_bytes, 0))?;
106
- snapshot.define_method("stderr_truncated", method!(Snapshot::stderr_truncated, 0))?;
107
- snapshot.define_method("wall_time", method!(Snapshot::wall_time, 0))?;
108
- snapshot.define_method("memory_peak", method!(Snapshot::memory_peak, 0))?;
109
- Ok(())
110
- }
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "capture"
4
- require_relative "usage"
5
-
6
- module Kobako
7
- # Kobako::Snapshot — per-invocation observable bundle returned from
8
- # +Kobako::Runtime#eval+ and +#run+.
9
- #
10
- # The magnus class (see ext/kobako/src/snapshot.rs) carries seven raw
11
- # readers: +return_bytes+, +stdout_bytes+, +stdout_truncated+,
12
- # +stderr_bytes+, +stderr_truncated+, +wall_time+, +memory_peak+. This
13
- # file reopens the class to add the Ruby-side helpers that pack those
14
- # raw fields into the user-facing value objects +Kobako::Capture+ and
15
- # +Kobako::Usage+ — the same shape +Kobako::Sandbox+ exposes to the
16
- # Host App.
17
- class Snapshot
18
- # Wrap the stdout capture pair (+stdout_bytes+, +stdout_truncated+)
19
- # as a +Kobako::Capture+ value object. The byte content never carries
20
- # a truncation sentinel; +#truncated?+ is the only way to observe
21
- # that the cap was hit.
22
- def stdout
23
- Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
24
- end
25
-
26
- # Wrap the stderr capture pair as a +Kobako::Capture+ value object.
27
- # Mirror of +#stdout+.
28
- def stderr
29
- Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
30
- end
31
-
32
- # Wrap the per-last-invocation usage pair (+wall_time+,
33
- # +memory_peak+) as a +Kobako::Usage+ value object.
34
- def usage
35
- Usage.new(wall_time: wall_time, memory_peak: memory_peak)
36
- end
37
- end
38
- end
@@ -1,15 +0,0 @@
1
- module Kobako
2
- class Snapshot
3
- def return_bytes: () -> String
4
- def stdout_bytes: () -> String
5
- def stdout_truncated: () -> bool
6
- def stderr_bytes: () -> String
7
- def stderr_truncated: () -> bool
8
- def wall_time: () -> Float
9
- def memory_peak: () -> Integer
10
-
11
- def stdout: () -> Kobako::Capture
12
- def stderr: () -> Kobako::Capture
13
- def usage: () -> Kobako::Usage
14
- end
15
- end