kobako 0.27.0 → 0.28.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +71 -0
  4. data/Cargo.lock +117 -140
  5. data/README.md +100 -125
  6. data/ROADMAP.md +23 -22
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +31 -0
  9. data/crates/kobako-runtime/Cargo.toml +2 -2
  10. data/crates/kobako-runtime/README.md +4 -3
  11. data/crates/kobako-runtime/src/error.rs +8 -9
  12. data/crates/kobako-runtime/src/lib.rs +1 -1
  13. data/crates/kobako-runtime/src/profile.rs +1 -1
  14. data/crates/kobako-runtime/src/runtime.rs +31 -17
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -10
  16. data/crates/kobako-runtime/src/yielder.rs +9 -6
  17. data/crates/kobako-transport/CHANGELOG.md +28 -0
  18. data/crates/kobako-transport/Cargo.toml +1 -1
  19. data/crates/kobako-transport/README.md +21 -12
  20. data/crates/kobako-transport/src/abi.rs +11 -14
  21. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  22. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  23. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  24. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  25. data/crates/kobako-transport/src/envelope/reply.rs +77 -53
  26. data/crates/kobako-transport/src/envelope/request.rs +295 -0
  27. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  28. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  29. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  30. data/crates/kobako-transport/src/envelope.rs +19 -12
  31. data/crates/kobako-transport/src/lib.rs +0 -2
  32. data/crates/kobako-wasmtime/CHANGELOG.md +44 -0
  33. data/crates/kobako-wasmtime/Cargo.toml +7 -5
  34. data/crates/kobako-wasmtime/README.md +2 -2
  35. data/crates/kobako-wasmtime/src/abi.rs +5 -8
  36. data/crates/kobako-wasmtime/src/ambient.rs +2 -2
  37. data/crates/kobako-wasmtime/src/cache.rs +25 -59
  38. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  39. data/crates/kobako-wasmtime/src/config.rs +11 -14
  40. data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
  41. data/crates/kobako-wasmtime/src/driver.rs +133 -86
  42. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  43. data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
  44. data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
  45. data/crates/kobako-wasmtime/src/invocation.rs +55 -372
  46. data/crates/kobako-wasmtime/src/io.rs +286 -0
  47. data/crates/kobako-wasmtime/src/lib.rs +3 -24
  48. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  49. data/crates/kobako-wasmtime/src/trap.rs +32 -45
  50. data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
  51. data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
  52. data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
  53. data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
  54. data/data/kobako.wasm +0 -0
  55. data/ext/kobako/Cargo.toml +2 -2
  56. data/ext/kobako/src/runtime/bridge.rs +38 -47
  57. data/ext/kobako/src/runtime/errors.rs +26 -32
  58. data/ext/kobako/src/runtime/gvl.rs +4 -12
  59. data/ext/kobako/src/runtime.rs +169 -212
  60. data/lib/kobako/capture.rb +1 -4
  61. data/lib/kobako/catalog/extensions.rb +21 -59
  62. data/lib/kobako/catalog/flags.rb +41 -0
  63. data/lib/kobako/catalog/handles.rb +7 -54
  64. data/lib/kobako/catalog/services.rb +7 -61
  65. data/lib/kobako/catalog/snippets.rb +42 -66
  66. data/lib/kobako/catalog.rb +2 -10
  67. data/lib/kobako/codec/decoder.rb +9 -28
  68. data/lib/kobako/codec/encoder.rb +3 -22
  69. data/lib/kobako/codec/error.rb +6 -27
  70. data/lib/kobako/codec/ext_types.rb +72 -94
  71. data/lib/kobako/codec/handle_walk.rb +65 -130
  72. data/lib/kobako/codec/nesting.rb +3 -8
  73. data/lib/kobako/codec/state.rb +4 -10
  74. data/lib/kobako/codec/utils.rb +6 -34
  75. data/lib/kobako/codec.rb +3 -22
  76. data/lib/kobako/constant_name.rb +8 -0
  77. data/lib/kobako/context.rb +51 -112
  78. data/lib/kobako/errors.rb +51 -127
  79. data/lib/kobako/execution.rb +16 -21
  80. data/lib/kobako/extension.rb +23 -40
  81. data/lib/kobako/handle.rb +7 -12
  82. data/lib/kobako/outcome.rb +51 -80
  83. data/lib/kobako/payload/arguments.rb +1 -16
  84. data/lib/kobako/payload.rb +4 -12
  85. data/lib/kobako/pool.rb +23 -53
  86. data/lib/kobako/runtime.rb +7 -21
  87. data/lib/kobako/sandbox.rb +96 -133
  88. data/lib/kobako/sandbox_options.rb +38 -84
  89. data/lib/kobako/snippet/binary.rb +1 -16
  90. data/lib/kobako/snippet/source.rb +1 -16
  91. data/lib/kobako/snippet.rb +1 -9
  92. data/lib/kobako/transport/call.rb +1 -14
  93. data/lib/kobako/transport/dispatcher.rb +112 -195
  94. data/lib/kobako/transport/error.rb +6 -20
  95. data/lib/kobako/transport/exposure.rb +5 -10
  96. data/lib/kobako/transport/reflection.rb +39 -50
  97. data/lib/kobako/transport/run.rb +17 -71
  98. data/lib/kobako/transport/yielder.rb +21 -73
  99. data/lib/kobako/transport.rb +3 -15
  100. data/lib/kobako/unresolved.rb +4 -11
  101. data/lib/kobako/usage.rb +2 -6
  102. data/lib/kobako/version.rb +1 -1
  103. data/sig/kobako/catalog/extensions.rbs +2 -2
  104. data/sig/kobako/catalog/flags.rbs +17 -0
  105. data/sig/kobako/catalog/services.rbs +0 -6
  106. data/sig/kobako/catalog/snippets.rbs +4 -4
  107. data/sig/kobako/codec/ext_types.rbs +10 -8
  108. data/sig/kobako/codec/handle_walk.rbs +9 -7
  109. data/sig/kobako/codec/utils.rbs +2 -2
  110. data/sig/kobako/codec.rbs +2 -2
  111. data/sig/kobako/constant_name.rbs +3 -0
  112. data/sig/kobako/context.rbs +2 -3
  113. data/sig/kobako/errors.rbs +6 -0
  114. data/sig/kobako/outcome.rbs +6 -8
  115. data/sig/kobako/runtime.rbs +12 -8
  116. data/sig/kobako/sandbox.rbs +4 -0
  117. data/sig/kobako/sandbox_options.rbs +2 -6
  118. data/sig/kobako/transport/dispatcher.rbs +14 -18
  119. data/sig/kobako/transport/reflection.rbs +5 -3
  120. data/sig/kobako/transport/run.rbs +1 -3
  121. data/sig/kobako/transport/yielder.rbs +3 -3
  122. data/sig/kobako/transport.rbs +7 -6
  123. metadata +16 -4
  124. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  125. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  126. data/rust-toolchain.toml +0 -15
@@ -1,273 +0,0 @@
1
- //! Per-invocation byte-shuttle between the host and guest linear memory:
2
- //! it resolves the required `memory` / ABI-export handles, writes the
3
- //! `#run` envelope into a freshly allocated guest buffer, builds the
4
- //! stdin frame stream plus stdout / stderr capture pipes for the WASI
5
- //! context, and reads the OUTCOME_BUFFER back out. The driver owns no
6
- //! wire codec — these helpers move raw bytes; the frontend decodes them.
7
-
8
- use wasmtime::{AsContextMut, Memory, Store as WtStore, TypedFunc};
9
- use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
10
- use wasmtime_wasi::WasiCtxBuilder;
11
-
12
- use crate::config::Config;
13
- use crate::exports::Exports;
14
- use crate::invocation::Invocation;
15
- use crate::{ambient, capture, guest_mem};
16
- use kobako_runtime::error::{InvokeError, SetupError, Trap};
17
- use kobako_runtime::profile::Profile;
18
- use kobako_transport::abi::FRAME_LEN_SIZE;
19
-
20
- /// Return the resolved `memory` export handle, or a `Trap` when the loaded
21
- /// module exports no linear memory — the "not a Kobako-shaped runtime"
22
- /// failure mode (`guest_mem::SANDBOX_RUNTIME_NOT_KOBAKO`).
23
- fn require_memory(exports: &Exports) -> Result<Memory, Trap> {
24
- exports
25
- .memory
26
- .ok_or_else(|| Trap::Other(guest_mem::SANDBOX_RUNTIME_NOT_KOBAKO.to_string()))
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
- /// Returns a `Trap` when the allocation hook is missing or itself traps
33
- /// (an engine fault), and a runtime-intact `SetupError` when the hook runs
34
- /// but cannot reserve the buffer (`__kobako_alloc` returns 0). The ext
35
- /// boundary maps these to `Kobako::TrapError` / `Kobako::SandboxError`.
36
- pub(crate) fn write_envelope(
37
- store: &mut WtStore<Invocation>,
38
- exports: &Exports,
39
- envelope: &[u8],
40
- ) -> Result<(i32, i32), InvokeError> {
41
- let len_i32 = guest_mem::checked_payload_len(envelope.len())
42
- .map_err(|msg| Trap::Other(msg.to_string()))?;
43
-
44
- let alloc = require_export(exports.alloc.as_ref())?;
45
- let memory = require_memory(exports)?;
46
-
47
- let ptr = alloc
48
- .call(store.as_context_mut(), len_i32 as u32)
49
- .map_err(|e| Trap::Other(format!("failed to allocate input buffer: {e}")))?;
50
- if ptr == 0 {
51
- return Err(SetupError::Intact(
52
- "could not allocate input buffer (out of memory)".to_string(),
53
- )
54
- .into());
55
- }
56
- let data = memory.data_mut(store.as_context_mut());
57
- let range = guest_mem::guest_buffer_range(ptr as usize, envelope.len(), data.len())
58
- .map_err(|msg| Trap::Other(msg.to_string()))?;
59
- data[range].copy_from_slice(envelope);
60
-
61
- Ok((ptr as i32, len_i32))
62
- }
63
-
64
- /// Build the per-invocation WASI context with stdin carrying every frame
65
- /// in `frames` (each prefixed by its 4-byte big-endian u32 length —
66
- /// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
67
- /// pipes, and install it on the invocation's Store. `#eval` passes three
68
- /// frames (preamble, source, snippets), `#run` passes two (preamble,
69
- /// snippets — the Run envelope arrives via linear memory
70
- /// instead). Each output pipe is sized at `cap + 1` so
71
- /// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
72
- /// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
73
- /// rely on `memory_limit` for the real ceiling.
74
- /// Returns a `Trap` when any frame exceeds the 16 MiB cap that keeps its
75
- /// `u32` length prefix from wrapping (boundary → `Kobako::TrapError`).
76
- pub(crate) fn install_wasi_frames(
77
- store: &mut WtStore<Invocation>,
78
- config: &Config,
79
- frames: &[&[u8]],
80
- ) -> Result<(), Trap> {
81
- // Every frame carries the same 16 MiB cap as the `#run` envelope
82
- // (`write_envelope`): the length prefix is a `u32`, so a frame past
83
- // the cap would silently wrap and corrupt the stdin frame stream.
84
- for &frame in frames {
85
- guest_mem::checked_payload_len(frame.len()).map_err(|msg| Trap::Other(msg.to_string()))?;
86
- }
87
-
88
- let total: usize = frames.iter().map(|&f| FRAME_LEN_SIZE + f.len()).sum();
89
- let mut stdin_content: Vec<u8> = Vec::with_capacity(total);
90
- for &frame in frames {
91
- stdin_content.extend_from_slice(&(frame.len() as u32).to_be_bytes());
92
- stdin_content.extend_from_slice(frame);
93
- }
94
-
95
- let stdin_pipe = MemoryInputPipe::new(stdin_content);
96
- let stdout_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stdout_limit));
97
- let stderr_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stderr_limit));
98
-
99
- let mut builder = WasiCtxBuilder::new();
100
- builder.stdin(stdin_pipe);
101
- builder.stdout(stdout_pipe.clone());
102
- builder.stderr(stderr_pipe.clone());
103
- // The requested profile decides the ambient-authority grant: the
104
- // hermetic rung denies the preview1 time and entropy imports (see
105
- // `ambient`), the permissive rung leaves the live WASI sources.
106
- // Filesystem, environment, and network stay absent on both rungs —
107
- // the builder grants none unless asked. The exhaustive match makes
108
- // a future ladder rung a compile error here, not a silent grant.
109
- match config.profile {
110
- Profile::Hermetic => {
111
- builder.wall_clock(ambient::FrozenWallClock);
112
- builder.monotonic_clock(ambient::FrozenMonotonicClock);
113
- builder.secure_random(ambient::deterministic_rng());
114
- }
115
- Profile::Permissive => {}
116
- }
117
- let wasi = builder.build_p1();
118
-
119
- store
120
- .data_mut()
121
- .install_wasi(wasi, stdout_pipe, stderr_pipe);
122
- Ok(())
123
- }
124
-
125
- /// Invoke `__kobako_take_outcome`, decode the packed `(ptr<<32)|len`
126
- /// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Returns a
127
- /// `Trap` (boundary → `Kobako::TrapError`) when the export is missing,
128
- /// `len` exceeds the 16 MiB single-dispatch cap, the `ptr`/`len`
129
- /// arithmetic overflows, the slice falls outside live memory, or the
130
- /// `memory` export itself is absent.
131
- pub(crate) fn fetch_outcome_bytes(
132
- store: &mut WtStore<Invocation>,
133
- exports: &Exports,
134
- ) -> Result<Vec<u8>, Trap> {
135
- let take = require_export(exports.take_outcome.as_ref())?;
136
- let mem = require_memory(exports)?;
137
-
138
- let packed = take
139
- .call(store.as_context_mut(), ())
140
- .map_err(|e| Trap::Other(format!("failed to read the Sandbox result: {e}")))?;
141
- let (ptr, len) = guest_mem::unpack_outcome_packed(packed);
142
- if len > kobako_transport::abi::MAX_DISPATCH_PAYLOAD {
143
- return Err(Trap::Other(
144
- "result payload exceeds the 16 MiB limit".to_string(),
145
- ));
146
- }
147
-
148
- let data = mem.data(store.as_context_mut());
149
- let range = guest_mem::guest_buffer_range(ptr, len, data.len())
150
- .map_err(|msg| Trap::Other(format!("the Sandbox result is out of bounds: {msg}")))?;
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
- /// Return the resolved `TypedFunc` for an ABI export, or a `Trap`
165
- /// (boundary → `Kobako::TrapError`) when the option is `None`. Both
166
- /// run-path methods (`#eval`, `#run`) plus the `build_snapshot` readout
167
- /// that drains `OUTCOME_BUFFER` share the same "missing export" handling;
168
- /// this helper collapses those sites onto one safe entry. The user-facing
169
- /// message is intentionally export-agnostic (see
170
- /// `SANDBOX_RUNTIME_MISSING_HOOKS`) — the ABI symbol name is not
171
- /// actionable to callers, so it is not threaded in.
172
- pub(crate) fn require_export<Params, Results>(
173
- export: Option<&TypedFunc<Params, Results>>,
174
- ) -> Result<&TypedFunc<Params, Results>, Trap>
175
- where
176
- Params: wasmtime::WasmParams,
177
- Results: wasmtime::WasmResults,
178
- {
179
- export.ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_MISSING_HOOKS.to_string()))
180
- }
181
-
182
- #[cfg(test)]
183
- mod tests {
184
- //! Witness the profile rung split at the WASI boundary: one probe
185
- //! module reads `wasi:clocks` / `wasi:random` through the context
186
- //! `install_wasi_frames` builds — frozen under `Hermetic`, live
187
- //! under `Permissive`.
188
- use wasmtime::{Linker, Module};
189
- use wasmtime_wasi::p1;
190
-
191
- use super::*;
192
- use crate::cache::shared_engine;
193
-
194
- /// Preview1 probe with one export per ambient source: `clock_ns`
195
- /// reads the realtime clock, `random_word` reads eight entropy bytes.
196
- const AMBIENT_PROBE_WAT: &str = r#"
197
- (module
198
- (import "wasi_snapshot_preview1" "clock_time_get"
199
- (func $clock (param i32 i64 i32) (result i32)))
200
- (import "wasi_snapshot_preview1" "random_get"
201
- (func $random (param i32 i32) (result i32)))
202
- (memory (export "memory") 1)
203
- (func (export "clock_ns") (result i64)
204
- (drop (call $clock (i32.const 0) (i64.const 1) (i32.const 0)))
205
- (i64.load (i32.const 0)))
206
- (func (export "random_word") (result i64)
207
- (drop (call $random (i32.const 8) (i32.const 8)))
208
- (i64.load (i32.const 8))))
209
- "#;
210
-
211
- /// Instantiate the probe over a WASI context built at `profile` and
212
- /// return the `(clock, random)` readings it observes.
213
- fn probe_ambient(profile: Profile) -> (i64, i64) {
214
- let engine = shared_engine().expect("shared engine must be constructible");
215
- let config = Config {
216
- timeout: None,
217
- memory_limit: None,
218
- stdout_limit: None,
219
- stderr_limit: None,
220
- profile,
221
- };
222
- let mut store = WtStore::new(engine, Invocation::new(None));
223
- store.set_epoch_deadline(crate::trap::NO_TIMEOUT_EPOCH_DELTA);
224
- install_wasi_frames(&mut store, &config, &[]).expect("WASI context must install");
225
-
226
- let mut linker: Linker<Invocation> = Linker::new(engine);
227
- p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
228
- .expect("WASI imports must link");
229
- let module = Module::new(engine, AMBIENT_PROBE_WAT).expect("probe module must compile");
230
- let instance = linker
231
- .instantiate(&mut store, &module)
232
- .expect("probe module must instantiate");
233
-
234
- let read = |store: &mut WtStore<Invocation>, name: &str| -> i64 {
235
- instance
236
- .get_typed_func::<(), i64>(store.as_context_mut(), name)
237
- .expect("probe export must resolve")
238
- .call(store.as_context_mut(), ())
239
- .expect("probe export must run")
240
- };
241
- let clock = read(&mut store, "clock_ns");
242
- let random = read(&mut store, "random_word");
243
- (clock, random)
244
- }
245
-
246
- // @behavior RT-047 RT-048
247
- #[test]
248
- fn hermetic_denies_ambient_time_and_entropy() {
249
- let (clock, random) = probe_ambient(Profile::Hermetic);
250
- assert_eq!(
251
- clock, 0,
252
- "a hermetic guest's wasi:clocks must read the Unix epoch, not host time"
253
- );
254
- assert_eq!(
255
- random, 0,
256
- "a hermetic guest's wasi:random must yield the constant stream, not host entropy"
257
- );
258
- }
259
-
260
- // @behavior RT-049 RT-050
261
- #[test]
262
- fn permissive_grants_live_ambient_time_and_entropy() {
263
- let (clock, random) = probe_ambient(Profile::Permissive);
264
- assert!(
265
- clock > 0,
266
- "a permissive guest's wasi:clocks must read live host time"
267
- );
268
- assert_ne!(
269
- random, 0,
270
- "a permissive guest's wasi:random must yield host entropy"
271
- );
272
- }
273
- }
data/rust-toolchain.toml DELETED
@@ -1,15 +0,0 @@
1
- # Pin the Rust toolchain so local builds and CI stay byte-identical.
2
- # The wasm32-wasip1 crt1-command.o references __wasi_init_tp from 1.96 onward;
3
- # vendored wasi-sdk 33's libc.a supplies that symbol, so the two move together.
4
- # Bump this in lockstep with the wasi-sdk toolchain beni vendors
5
- # (`rake beni:build`, wired in the Rakefile's Beni::Tasks block).
6
- # The floor is 1.98: rustc strips an Apple binary with its own Mach-O writer,
7
- # and only from that release does the stripped string pool keep the alignment
8
- # dyld requires from macOS 26 onward — below it, `strip = true` in the release
9
- # profile yields an ext no dlopen accepts.
10
- # This file is the single source of the channel; the CI workflows read it.
11
- [toolchain]
12
- channel = "1.98.0"
13
- components = ["clippy", "rustfmt"]
14
- targets = ["wasm32-wasip1"]
15
- profile = "minimal"