kobako 0.7.0 → 0.9.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.
data/README.md CHANGED
@@ -283,6 +283,29 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
283
283
 
284
284
  Use the source form for snippets authored in your repo (compile errors fail fast at `#preload`); use the bytecode form when snippets ship as build artifacts from a separate `mrbc` pipeline. Both replay through the same per-invocation path.
285
285
 
286
+ ## Security
287
+
288
+ kobako isolates the guest, but **what it may reach is whatever you `bind`** — and `bind`
289
+ exposes *every* public method of the object. So bind a purpose-built object scoped to the
290
+ task, not a capable one whose other methods leak more than you intend.
291
+
292
+ ```ruby
293
+ class ThemeReader # only #color is reachable; AppConfig.secret_key is not
294
+ def color = AppConfig.theme.color
295
+ end
296
+
297
+ sandbox = Kobako::Sandbox.new
298
+ sandbox.define(:Cfg).bind(:Settings, ThemeReader.new) # not: bind(:Settings, AppConfig)
299
+
300
+ sandbox.eval('Cfg::Settings.color') # => "#3366ff" — every other method raises NoMethodError
301
+ ```
302
+
303
+ Guest code can name any `<Namespace>::<Member>` path, but a forged name only resolves to
304
+ something you bound — the real authorization gate is this host-side allowlist. Give each
305
+ trust context its own Sandbox, and see [`docs/security.md`](docs/security.md) for the rest
306
+ as security-design concerns: validating untrusted input, default-deny external effects,
307
+ and controlling the return surface.
308
+
286
309
  ## Performance
287
310
 
288
311
  Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values vary by hardware but ratios are stable across machines. Full numbers, methodology, and the +10%-regression gate live in [`benchmark/README.md`](benchmark/README.md).
@@ -299,6 +322,8 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
299
322
 
300
323
  Construct one Sandbox at boot so the ~600 ms JIT cost lands off the request hot path. `ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput stays around 7-8k `#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.
301
324
 
325
+ 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 (`#10` 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.
326
+
302
327
  ```bash
303
328
  bundle exec rake bench # six gated regression benchmarks (~5-8 min)
304
329
  ```
@@ -312,7 +337,7 @@ bin/setup # install dependencies
312
337
  bundle exec rake # default: compile + test + rubocop + steep
313
338
  ```
314
339
 
315
- Building from source requires a WASI-capable Rust toolchain in addition to the standard host toolchain; the first compile walks the full vendor / mruby / wasm chain. See [`CLAUDE.md`](CLAUDE.md) for the rake task map and pipeline layout. `bin/console` opens an IRB session with the gem preloaded; `bundle exec rake install` installs the local checkout as a gem.
340
+ Building from source requires a WASI-capable Rust toolchain in addition to the standard host toolchain; the first compile walks the full chain — the [beni](https://github.com/elct9620/beni) gem vendors wasi-sdk + mruby and builds `libmruby.a` (`rake beni:build`), then `rake wasm:build` produces the Guest Binary. See [`CLAUDE.md`](CLAUDE.md) for the rake task map and pipeline layout. `bin/console` opens an IRB session with the gem preloaded; `bundle exec rake install` installs the local checkout as a gem.
316
341
 
317
342
  ## Contributing
318
343
 
data/data/kobako.wasm CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "kobako"
3
- version = "0.7.0"
3
+ version = "0.9.0"
4
4
  edition = "2021"
5
5
  authors = ["Aotokitsuruya <contact@aotoki.me>"]
6
6
  license = "Apache-2.0"
@@ -14,12 +14,10 @@ magnus = { version = "0.8.2" }
14
14
  # wasmtime — host-side embedder for kobako.wasm. We disable default-features
15
15
  # and opt back in only what kobako needs: a Cranelift-backed runtime that can
16
16
  # compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
17
- # feature
18
- # so test fixtures can be expressed as text. WASI integration is layered on
19
- # later via `wasmtime-wasi` once stdin/stdout wiring is needed (item #16).
17
+ # feature so test fixtures can be expressed as text.
20
18
  # `cache` / `parallel-compilation` / `pooling` / `component-model` / `async`
21
19
  # are intentionally off — kobako runs short-lived synchronous sandboxes.
22
- wasmtime = { version = "44.0.1", default-features = false, features = [
20
+ wasmtime = { version = "45.0.0", default-features = false, features = [
23
21
  "cranelift",
24
22
  "runtime",
25
23
  "gc",
@@ -33,4 +31,4 @@ wasmtime = { version = "44.0.1", default-features = false, features = [
33
31
  # WasiCtxBuilder + preview1 adapter which wires fd 1/2 to pipes. We omit
34
32
  # `p2` (component-model) and `p0`/`p3` (async) because kobako runs
35
33
  # synchronous sandboxes only.
36
- wasmtime-wasi = { version = "44.0.1", default-features = false, features = ["p1"] }
34
+ wasmtime-wasi = { version = "45.0.0", default-features = false, features = ["p1"] }
@@ -1,7 +1,7 @@
1
1
  //! Host-side dispatch for the `__kobako_dispatch` import.
2
2
  //!
3
3
  //! When the guest invokes the wasm import declared in
4
- //! `wasm/kobako-wasm/src/abi.rs`, wasmtime calls back into the host
4
+ //! `wasm/kobako-core/src/abi.rs`, wasmtime calls back into the host
5
5
  //! through the closure built in `super::Runtime::build`.
6
6
  //! That closure delegates here. The dispatcher (docs/behavior.md B-12 / B-13):
7
7
  //!
@@ -180,9 +180,9 @@ fn try_handle(
180
180
  write_response(caller, &resp_bytes)
181
181
  }
182
182
 
183
- /// Invoke the Ruby-side dispatch +Proc+ with the request bytes and return
183
+ /// Invoke the Ruby-side dispatch `Proc` with the request bytes and return
184
184
  /// the encoded Response bytes. The Proc is contracted to fold every
185
- /// dispatch failure into a +Response.err+ envelope (see
185
+ /// dispatch failure into a `Response.err` envelope (see
186
186
  /// `Kobako::Transport::Dispatcher.dispatch`), so reaching the error
187
187
  /// branch is itself a wire-layer fault rather than a normal control path.
188
188
  fn invoke_on_dispatch(
@@ -127,7 +127,7 @@ pub(super) fn guest_buffer_range(
127
127
 
128
128
  /// Unpack the `(ptr, len)` u64 returned by `__kobako_take_outcome`:
129
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`.
130
+ /// `unpack_u64` in `wasm/kobako-core/src/abi.rs`.
131
131
  pub(super) fn unpack_outcome_packed(packed: u64) -> (usize, usize) {
132
132
  let ptr = (packed >> 32) as u32 as usize;
133
133
  let len = packed as u32 as usize;
@@ -82,7 +82,7 @@ impl Invocation {
82
82
  self.stderr_pipe = Some(stderr);
83
83
  }
84
84
 
85
- /// Register the Ruby-side dispatch +Proc+. From this point on, every
85
+ /// Register the Ruby-side dispatch `Proc`. From this point on, every
86
86
  /// `__kobako_dispatch` host import invocation calls the Proc with the
87
87
  /// request bytes and expects encoded Response bytes back.
88
88
  pub(super) fn bind_on_dispatch(&mut self, proc_value: Opaque<Value>) {
@@ -325,9 +325,9 @@ pub(crate) struct MemoryLimitTrap {
325
325
  }
326
326
 
327
327
  impl MemoryLimitTrap {
328
- /// Construct a trap with the given +desired+ / +limit+ pair. Used
328
+ /// Construct a trap with the given `desired` / `limit` pair. Used
329
329
  /// internally by `MemoryLimiter::memory_growing` in production and
330
- /// by the sibling-module +classify_trap+ unit tests to materialise
330
+ /// by the sibling-module `classify_trap` unit tests to materialise
331
331
  /// a representative error for downcast routing.
332
332
  #[cfg(test)]
333
333
  pub(super) fn new(desired: usize, limit: usize) -> Self {
@@ -65,9 +65,15 @@ use self::config::Config;
65
65
  use self::exports::Exports;
66
66
  use self::invocation::{Invocation, StoreCell};
67
67
 
68
- /// Copy the bytes of +s+ into a fresh `Vec<u8>`. Single safe entry to
69
- /// what would otherwise be an inline +unsafe { rstring.as_slice() }
70
- /// .to_vec()+ duplicated at every host-↔-guest boundary. The borrow
68
+ /// The wire ABI version this host implements (docs/wire-codec.md § ABI
69
+ /// Version). A Guest Binary is accepted only when its
70
+ /// `__kobako_abi_version` export reports the same value (B-40 / E-42);
71
+ /// the guest-side mirror is `kobako_core::abi::ABI_VERSION`.
72
+ const ABI_VERSION: u32 = 1;
73
+
74
+ /// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
75
+ /// what would otherwise be an inline `unsafe { rstring.as_slice() }
76
+ /// .to_vec()` duplicated at every host-↔-guest boundary. The borrow
71
77
  /// does not outlive this call, so no Ruby allocation can move the
72
78
  /// underlying RString between the borrow and the copy — the safety
73
79
  /// invariant the inline form relied on is established once here.
@@ -86,12 +92,12 @@ pub(crate) fn rstring_to_vec(s: RString) -> Vec<u8> {
86
92
  // verb prefix and lets the subclass identity flow through unchanged.
87
93
  // ---------------------------------------------------------------------------
88
94
 
89
- /// Resolve `Kobako::<name>` as an +ExceptionClass+ — the shared body of
95
+ /// Resolve `Kobako::<name>` as an `ExceptionClass` — the shared body of
90
96
  /// every error-class `Lazy` below, which differ only in the constant
91
97
  /// name. The constants are guaranteed present by the time any of these
92
98
  /// lazies first resolve (`lib/kobako/errors.rb` loads the hierarchy before
93
99
  /// the ext raises into it), so a missing constant is a build / wiring bug
94
- /// and the +unwrap+ is the correct fail-fast.
100
+ /// and the `unwrap` is the correct fail-fast.
95
101
  fn kobako_error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
96
102
  let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
97
103
  kobako.const_get(name).unwrap()
@@ -115,8 +121,8 @@ pub(crate) static MEMORY_LIMIT_ERROR: Lazy<ExceptionClass> =
115
121
  pub(crate) static SANDBOX_ERROR: Lazy<ExceptionClass> =
116
122
  Lazy::new(|ruby| kobako_error_class(ruby, "SandboxError"));
117
123
 
118
- /// Build a +MagnusError+ in +class+ carrying +msg+ — the shared body of
119
- /// the named +*_err+ constructors below, which differ only in which
124
+ /// Build a `MagnusError` in `class` carrying `msg` — the shared body of
125
+ /// the named `*_err` constructors below, which differ only in which
120
126
  /// error-class `Lazy` they target.
121
127
  fn error_in(ruby: &Ruby, class: &Lazy<ExceptionClass>, msg: impl Into<String>) -> MagnusError {
122
128
  MagnusError::new(ruby.get_inner(class), msg.into())
@@ -349,6 +355,8 @@ impl Runtime {
349
355
  .map_err(|e| trap::instantiate_err(&ruby, e))?
350
356
  };
351
357
 
358
+ Self::validate_abi_version(&instance, &store_cell, &ruby)?;
359
+
352
360
  let exports = Exports::resolve(&instance, &store_cell);
353
361
 
354
362
  Ok(Self {
@@ -364,9 +372,51 @@ impl Runtime {
364
372
  })
365
373
  }
366
374
 
367
- /// Register the Ruby-side dispatch +Proc+ on the active Invocation.
368
- /// Bound to Ruby as +Kobako::Runtime#on_dispatch=+. From this point on,
369
- /// every +__kobako_dispatch+ host import invocation calls the Proc
375
+ /// Probe the guest's `__kobako_abi_version` export once at
376
+ /// construction and require equality with `ABI_VERSION`
377
+ /// (docs/behavior.md B-40). An absent export or a non-equal value is
378
+ /// E-42 — a deterministic artifact fault raised as
379
+ /// `Kobako::SetupError`.
380
+ fn validate_abi_version(
381
+ instance: &WtInstance,
382
+ store: &StoreCell,
383
+ ruby: &Ruby,
384
+ ) -> Result<(), MagnusError> {
385
+ let mut store_ref = store.borrow_mut();
386
+ let mut ctx = store_ref.as_context_mut();
387
+ let probe = instance
388
+ .get_typed_func::<(), u32>(&mut ctx, "__kobako_abi_version")
389
+ .map_err(|_| {
390
+ setup_err(
391
+ ruby,
392
+ format!(
393
+ "the Guest Binary does not export __kobako_abi_version; \
394
+ rebuild it against ABI version {ABI_VERSION}"
395
+ ),
396
+ )
397
+ })?;
398
+ let reported = probe.call(&mut ctx, ()).map_err(|e| {
399
+ setup_err(
400
+ ruby,
401
+ format!("failed to read the Guest Binary's ABI version: {e}"),
402
+ )
403
+ })?;
404
+ if reported != ABI_VERSION {
405
+ return Err(setup_err(
406
+ ruby,
407
+ format!(
408
+ "the Guest Binary reports ABI version {reported}, but this host \
409
+ implements ABI version {ABI_VERSION}; rebuild the Guest Binary \
410
+ against the host's version"
411
+ ),
412
+ ));
413
+ }
414
+ Ok(())
415
+ }
416
+
417
+ /// Register the Ruby-side dispatch `Proc` on the active Invocation.
418
+ /// Bound to Ruby as `Kobako::Runtime#on_dispatch=`. From this point on,
419
+ /// every `__kobako_dispatch` host import invocation calls the Proc
370
420
  /// with the request bytes and writes the returned Response bytes back
371
421
  /// into guest memory (docs/behavior.md B-12).
372
422
  pub(crate) fn set_on_dispatch(&self, proc_value: Value) -> Result<(), MagnusError> {
@@ -390,14 +440,14 @@ impl Runtime {
390
440
  /// export with `args_bytes` as the yield-arguments payload, and
391
441
  /// return the YieldResponse bytes the guest produced (B-24).
392
442
  ///
393
- /// Bound to Ruby as +Kobako::Runtime#yield_to_active_invocation+.
443
+ /// Bound to Ruby as `Kobako::Runtime#yield_to_active_invocation`.
394
444
  /// Recovers the dispatcher's `&mut Caller` from the per-thread
395
445
  /// Invocation slot (SPEC.md Single-Invocation Slot) — the host is
396
446
  /// already inside a `__kobako_dispatch` callback, so the Caller
397
447
  /// parked on the Rust stack is the same one the Sandbox-level
398
448
  /// `#eval` / `#run` is driving. Invoked from the host-side yield
399
449
  /// proxy that the dispatcher hands to Service methods (B-23 / B-24);
400
- /// raises +Kobako::TrapError+ when called outside an active dispatch
450
+ /// raises `Kobako::TrapError` when called outside an active dispatch
401
451
  /// frame, or when any of the underlying allocation / write / call /
402
452
  /// read steps fails.
403
453
  pub(crate) fn yield_to_active_invocation(
@@ -432,8 +482,8 @@ impl Runtime {
432
482
  /// and return a `Snapshot` bundling every per-invocation observable.
433
483
  ///
434
484
  /// Rebuilds the WASI context with fresh stdin / stdout / stderr pipes
435
- /// (the three-frame stdin protocol carries +preamble+, +source+, then
436
- /// +snippets+ — docs/wire-codec.md § Invocation channels), then
485
+ /// (the three-frame stdin protocol carries `preamble`, `source`, then
486
+ /// `snippets` — docs/wire-codec.md § Invocation channels), then
437
487
  /// invokes `__kobako_eval`. Per-invocation caps (docs/behavior.md
438
488
  /// B-01) are primed here: the wall-clock deadline is stamped into
439
489
  /// `Invocation` and the epoch deadline is set to fire at the next
@@ -467,10 +517,10 @@ impl Runtime {
467
517
  ///
468
518
  /// Rebuilds the WASI context with the two-frame stdin protocol
469
519
  /// (preamble + snippets; no user source frame — docs/wire-codec.md
470
- /// § Invocation channels), copies +envelope+ bytes into guest linear
520
+ /// § Invocation channels), copies `envelope` bytes into guest linear
471
521
  /// memory via `__kobako_alloc`, and calls `__kobako_run(env_ptr,
472
522
  /// env_len)`. Per-invocation cap semantics match `Runtime::eval`.
473
- /// Raises +Kobako::TrapError+ ("alloc returned 0") when guest
523
+ /// Raises `Kobako::TrapError` ("alloc returned 0") when guest
474
524
  /// allocation fails (docs/behavior.md E-31).
475
525
  pub(crate) fn run(
476
526
  &self,
@@ -596,7 +646,7 @@ impl Runtime {
596
646
  /// The mruby image's declared initial allocation and the high-water
597
647
  /// mark left by prior invocations on the same Sandbox are folded
598
648
  /// into the baseline rather than the budget — only `memory.grow`
599
- /// past +baseline+ counts against `memory_limit`.
649
+ /// past `baseline` counts against `memory_limit`.
600
650
  ///
601
651
  /// Also stamps the wall-clock entry instant for the
602
652
  /// docs/behavior.md B-35 `wall_time` measurement. The bracket
@@ -636,11 +686,11 @@ impl Runtime {
636
686
  store_ref.data_mut().disarm_memory_cap();
637
687
  }
638
688
 
639
- /// Allocate a +len+-byte buffer in guest linear memory via
640
- /// `__kobako_alloc`, copy +envelope+ into it, and return +(ptr, len)+
641
- /// as +i32+ values matching the `__kobako_run(env_ptr, env_len)` ABI.
642
- /// Raises +Kobako::TrapError+ when the allocation hook is missing or
643
- /// itself traps, and +Kobako::SandboxError+ when the hook runs but
689
+ /// Allocate a `len`-byte buffer in guest linear memory via
690
+ /// `__kobako_alloc`, copy `envelope` into it, and return `(ptr, len)`
691
+ /// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
692
+ /// Raises `Kobako::TrapError` when the allocation hook is missing or
693
+ /// itself traps, and `Kobako::SandboxError` when the hook runs but
644
694
  /// cannot reserve the buffer (`__kobako_alloc` returns 0,
645
695
  /// docs/behavior.md E-31) — an intact runtime, not an engine fault.
646
696
  fn write_envelope(&self, ruby: &Ruby, envelope: RString) -> Result<(i32, i32), MagnusError> {
@@ -676,10 +726,10 @@ impl Runtime {
676
726
  }
677
727
 
678
728
  /// Rebuild the WASI context with fresh stdin (carrying every frame in
679
- /// +frames+, each prefixed by its 4-byte big-endian u32 length —
729
+ /// `frames`, each prefixed by its 4-byte big-endian u32 length —
680
730
  /// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
681
- /// pipes. Called at the top of every guest invocation: +#eval+ passes
682
- /// three frames (preamble, source, snippets), +#run+ passes two
731
+ /// pipes. Called at the top of every guest invocation: `#eval` passes
732
+ /// three frames (preamble, source, snippets), `#run` passes two
683
733
  /// (preamble, snippets — the invocation envelope arrives via linear
684
734
  /// memory instead). Each output pipe is sized at `cap + 1` so
685
735
  /// `capture::clip_capture` can distinguish "wrote exactly cap
@@ -712,10 +762,10 @@ impl Runtime {
712
762
  .install_wasi(wasi, stdout_pipe, stderr_pipe);
713
763
  }
714
764
 
715
- /// Invoke `__kobako_take_outcome`, decode the packed +(ptr<<32)|len+
765
+ /// Invoke `__kobako_take_outcome`, decode the packed `(ptr<<32)|len`
716
766
  /// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Raises
717
- /// `Kobako::TrapError` when the export is missing, +len+ exceeds the
718
- /// 16 MiB single-dispatch cap, the +ptr+/+len+ arithmetic overflows,
767
+ /// `Kobako::TrapError` when the export is missing, `len` exceeds the
768
+ /// 16 MiB single-dispatch cap, the `ptr`/`len` arithmetic overflows,
719
769
  /// the slice falls outside live memory, or the `memory` export itself
720
770
  /// is absent.
721
771
  fn fetch_outcome_bytes(&self, ruby: &Ruby) -> Result<Vec<u8>, MagnusError> {
@@ -762,10 +812,10 @@ const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required
762
812
  const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
763
813
  "the loaded Wasm module is not a Kobako-compatible runtime";
764
814
 
765
- /// Return the cached +TypedFunc+ for an ABI export, or raise
766
- /// +Kobako::TrapError+ when the option is +None+. Both run-path
767
- /// methods (+#eval+, +#run+) plus the +build_snapshot+ readout that
768
- /// drains +OUTCOME_BUFFER+ share the same "missing export → Ruby
815
+ /// Return the cached `TypedFunc` for an ABI export, or raise
816
+ /// `Kobako::TrapError` when the option is `None`. Both run-path
817
+ /// methods (`#eval`, `#run`) plus the `build_snapshot` readout that
818
+ /// drains `OUTCOME_BUFFER` share the same "missing export → Ruby
769
819
  /// error" boilerplate; this helper collapses those sites onto one
770
820
  /// safe entry. The user-facing message is intentionally export-
771
821
  /// agnostic (see `SANDBOX_RUNTIME_MISSING_HOOKS`) — the ABI symbol
@@ -28,7 +28,7 @@ module Kobako
28
28
  # installs ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
29
29
  # The registry holds an injected +Catalog::Handles+ reference so
30
30
  # dispatch target resolution and host→guest auto-wrap share the same
31
- # Sandbox-owned allocator (docs/behavior.md B-19).
31
+ # Sandbox-owned allocator ({docs/behavior.md B-19}[link:../../../docs/behavior.md]).
32
32
  class Namespaces
33
33
  # Build a fresh registry. +handler+ is an internal seam that injects
34
34
  # a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
@@ -40,7 +40,8 @@ module Kobako
40
40
  @sealed = false
41
41
  end
42
42
 
43
- # Declare or retrieve the Namespace named +name+ (idempotent — docs/behavior.md B-10).
43
+ # Declare or retrieve the Namespace named +name+ (idempotent —
44
+ # {docs/behavior.md B-10}[link:../../../docs/behavior.md]).
44
45
  # +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
45
46
  # +Namespace::NAME_PATTERN+). Returns the +Kobako::Namespace+ for that
46
47
  # name, creating it if it does not exist. Raises +ArgumentError+ when
@@ -77,9 +77,7 @@ module Kobako
77
77
  )
78
78
  end
79
79
 
80
- # Symbol-to-name packer extracted to a real method so Steep can
81
- # resolve the proc shape without tripping on +lambda(&:name)+'s
82
- # +Symbol#to_proc+ inference path.
80
+ # Symbol-to-name packer for the ext-0x00 registration.
83
81
  def pack_symbol(symbol)
84
82
  symbol.name
85
83
  end
@@ -148,8 +146,7 @@ module Kobako
148
146
  # method when a nested ext 0x02 appears inside +details+. The recursion
149
147
  # is bounded by msgpack nesting depth — identical to nested Array /
150
148
  # Hash payloads — so no extra guard is needed. Do not switch back to
151
- # +factory.load+ to "simplify": that path bypasses UTF-8 validation
152
- # and re-opens the Decoder's special case for Fault (removed in M5).
149
+ # +factory.load+ to "simplify": that path bypasses UTF-8 validation.
153
150
  def unpack_fault(payload)
154
151
  Decoder.decode(payload) do |map|
155
152
  raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
@@ -43,14 +43,12 @@ module Kobako
43
43
  # stays {Kobako::Codec::Error} and never leaks +ArgumentError+ from
44
44
  # the Ruby standard library.
45
45
  #
46
- # Most construction sites no longer reach for this directly: a value
47
- # object built inside a {Decoder.decode} block has its
48
- # +ArgumentError+ mapped to {InvalidType} by the decoder's own
49
- # rescue. The lone remaining caller is {Factory#unpack_handle}, which
50
- # builds +Handle.restore+ from a raw 4-byte fixext payload without a
51
- # {Decoder.decode} call. Do not use it for general-purpose validation
52
- # outside the codec boundary — host-layer +ArgumentError+ values
53
- # should propagate unchanged.
46
+ # Reach for this only where a value object is constructed outside a
47
+ # {Decoder.decode} block, whose rescue already performs the same
48
+ # mapping (worked example: {Factory#unpack_handle} building
49
+ # +Handle.restore+ from a raw fixext payload). Do not use it for
50
+ # general-purpose validation outside the codec boundary
51
+ # host-layer +ArgumentError+ values should propagate unchanged.
54
52
  def with_boundary
55
53
  yield
56
54
  rescue ::ArgumentError => e
@@ -143,8 +141,7 @@ module Kobako
143
141
  end
144
142
  end
145
143
 
146
- # Predicate split out of {representable?} for cyclomatic
147
- # budget — the closed-set non-container branch. Returns +true+ for
144
+ # The non-container branch of {representable?}: returns +true+ for
148
145
  # the scalar leaves and an existing Handle. Not part of the
149
146
  # public surface; reach for {representable?} instead.
150
147
  def primitive_type?(value)
@@ -155,11 +152,10 @@ module Kobako
155
152
  end
156
153
  end
157
154
 
158
- # Predicate split out of {representable?} for cyclomatic
159
- # budget the container branch. Recurses into Array elements and
160
- # Hash key+value pairs through the public {representable?}.
161
- # Not part of the public surface; reach for {representable?}
162
- # instead.
155
+ # The container branch of {representable?}: recurses into Array
156
+ # elements and Hash key+value pairs through the public
157
+ # {representable?}. Not part of the public surface; reach for
158
+ # {representable?} instead.
163
159
  def container_representable?(value)
164
160
  case value
165
161
  when ::Array then value.all? { |element| Utils.representable?(element) }
data/lib/kobako/errors.rb CHANGED
@@ -26,9 +26,9 @@ module Kobako
26
26
  # * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
27
27
  # when the wasm runtime cannot be built from the
28
28
  # configured +wasm_path+ before any invocation runs
29
- # (docs/behavior.md E-40 / E-41). Not an invocation
30
- # outcome, so it never passes through the two-step
31
- # attribution decision.
29
+ # ({docs/behavior.md E-40 / E-41}[link:../../docs/behavior.md]).
30
+ # Not an invocation outcome, so it never passes
31
+ # through the two-step attribution decision.
32
32
  #
33
33
  # Subclasses pinned by docs/behavior.md Error Classes:
34
34
  #
data/lib/kobako/fault.rb CHANGED
@@ -22,10 +22,8 @@ module Kobako
22
22
  # exception class (RuntimeError, ArgumentError, Kobako::ServiceError, ...)
23
23
  # is the responsibility of the dispatch layer, not the codec.
24
24
  #
25
- # Built on the +class X < Data.define(...)+ subclass form so the
26
- # class body is fully Steep-visible; ruby/rbs upstream documents
27
- # this as the Steep-friendly shape and the +Style/DataInheritance+
28
- # cop is disabled on that basis (see +.rubocop.yml+).
25
+ # Built on the +class X < Data.define(...)+ subclass form (the
26
+ # Steep-friendly shape see +lib/kobako/outcome/panic.rb+).
29
27
  class Fault < Data.define(:type, :message, :details)
30
28
  VALID_TYPES = %w[runtime argument undefined].freeze
31
29
 
@@ -292,9 +292,9 @@ module Kobako
292
292
  #
293
293
  # The yielded block must return a +Kobako::Snapshot+ — i.e. the
294
294
  # value of +Runtime#eval+ / +#run+ (SPEC.md Internal Concepts →
295
- # Snapshot). The success path unpacks every observable from the
296
- # Snapshot in one go: +#stdout+ / +#stderr+ pack into +Capture+,
297
- # +#usage+ packs into +Usage+, +#return_bytes+ feeds +Outcome.decode+.
295
+ # Snapshot). The success path unpacks +#stdout+ / +#stderr+ into
296
+ # +Capture+ and feeds +#return_bytes+ to +Outcome.decode+; usage is
297
+ # populated by the +ensure+ readout ({#read_usage!}) on every outcome.
298
298
  # The rescue chain is the single trap-translation boundary —
299
299
  # configured-cap paths
300
300
  # ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
@@ -117,7 +117,7 @@ module Kobako
117
117
  end
118
118
  end
119
119
 
120
- # {docs/behavior.md B-16}[link:../../../docs/behavior.md] — An Kobako::Handle arriving as a positional or keyword
120
+ # {docs/behavior.md B-16}[link:../../../docs/behavior.md] — A Kobako::Handle arriving as a positional or keyword
121
121
  # argument identifies a host-side object previously allocated by a prior
122
122
  # transport call's Handle wrap (B-14). Resolve it back to the Ruby object before
123
123
  # the dispatch reaches +public_send+.
data/lib/kobako/usage.rb CHANGED
@@ -28,10 +28,8 @@ module Kobako
28
28
  # consumed. Before the first invocation +Sandbox#usage+ returns the
29
29
  # pre-invocation sentinel +Kobako::Usage::EMPTY+.
30
30
  #
31
- # Built on the +class X < Data.define(...)+ subclass form so the
32
- # class body is fully Steep-visible; ruby/rbs upstream documents this
33
- # as the Steep-friendly shape and the +Style/DataInheritance+ cop is
34
- # disabled on that basis (see +.rubocop.yml+).
31
+ # Built on the +class X < Data.define(...)+ subclass form (the
32
+ # Steep-friendly shape see +lib/kobako/outcome/panic.rb+).
35
33
  class Usage < Data.define(:wall_time, :memory_peak)
36
34
  # Pre-invocation sentinel ({docs/behavior.md B-35}[link:../../docs/behavior.md]).
37
35
  # Reused by +Sandbox+ before any invocation has run so callers do not
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -6,9 +6,82 @@
6
6
  ".": {
7
7
  "component": "kobako",
8
8
  "include-component-in-tag": false,
9
- "release-type": "ruby"
9
+ "release-type": "ruby",
10
+ "exclude-paths": ["wasm"]
11
+ },
12
+ "wasm/kobako-core": {
13
+ "component": "kobako-core",
14
+ "release-type": "rust",
15
+ "extra-files": [
16
+ {
17
+ "type": "toml",
18
+ "path": "/wasm/Cargo.lock",
19
+ "jsonpath": "$.package[?(@.name=='kobako-core')].version"
20
+ },
21
+ {
22
+ "type": "generic",
23
+ "path": "/wasm/kobako-core/README.md"
24
+ }
25
+ ]
26
+ },
27
+ "wasm/kobako": {
28
+ "component": "kobako-rs",
29
+ "release-type": "rust",
30
+ "extra-files": [
31
+ {
32
+ "type": "toml",
33
+ "path": "/wasm/Cargo.lock",
34
+ "jsonpath": "$.package[?(@.name=='kobako')].version"
35
+ },
36
+ {
37
+ "type": "toml",
38
+ "path": "/wasm/kobako/Cargo.toml",
39
+ "jsonpath": "$.dependencies['kobako-core'].version"
40
+ },
41
+ {
42
+ "type": "generic",
43
+ "path": "/wasm/kobako/README.md"
44
+ }
45
+ ]
46
+ },
47
+ "wasm/kobako-io": {
48
+ "component": "kobako-io",
49
+ "release-type": "rust",
50
+ "extra-files": [
51
+ {
52
+ "type": "toml",
53
+ "path": "/wasm/Cargo.lock",
54
+ "jsonpath": "$.package[?(@.name=='kobako-io')].version"
55
+ },
56
+ {
57
+ "type": "generic",
58
+ "path": "/wasm/kobako-io/README.md"
59
+ }
60
+ ]
61
+ },
62
+ "wasm/kobako-regexp": {
63
+ "component": "kobako-regexp",
64
+ "release-type": "rust",
65
+ "extra-files": [
66
+ {
67
+ "type": "toml",
68
+ "path": "/wasm/Cargo.lock",
69
+ "jsonpath": "$.package[?(@.name=='kobako-regexp')].version"
70
+ },
71
+ {
72
+ "type": "generic",
73
+ "path": "/wasm/kobako-regexp/README.md"
74
+ }
75
+ ]
10
76
  }
11
77
  },
78
+ "plugins": [
79
+ {
80
+ "type": "linked-versions",
81
+ "groupName": "kobako guest crates",
82
+ "components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-regexp"]
83
+ }
84
+ ],
12
85
  "extra-files": [
13
86
  {
14
87
  "type": "toml",
data/rust-toolchain.toml CHANGED
@@ -1,7 +1,8 @@
1
1
  # Pin the Rust toolchain so local builds and CI stay byte-identical.
2
2
  # The wasm32-wasip1 crt1-command.o references __wasi_init_tp from 1.96 onward;
3
3
  # vendored wasi-sdk 33's libc.a supplies that symbol, so the two move together.
4
- # Bump this in lockstep with WASI_SDK_VERSION in tasks/support/kobako_vendor.rb.
4
+ # Bump this in lockstep with the wasi-sdk toolchain beni vendors
5
+ # (`rake beni:build`, wired in the Rakefile's Beni::Tasks block).
5
6
  # This file is the single source of the channel; the CI workflows read it.
6
7
  [toolchain]
7
8
  channel = "1.96.0"
@@ -0,0 +1,4 @@
1
+ module Kobako
2
+ module Codec
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module Kobako
2
+ VERSION: String
3
+ end
data/sig/kobako.rbs CHANGED
@@ -1,3 +1,2 @@
1
1
  module Kobako
2
- VERSION: String
3
2
  end