kobako 0.10.0 → 0.11.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +7 -0
  4. data/Cargo.lock +1 -1
  5. data/README.md +14 -7
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +2 -2
  8. data/ext/kobako/src/runtime/ambient.rs +1 -1
  9. data/ext/kobako/src/runtime/cache.rs +3 -3
  10. data/ext/kobako/src/runtime/capture.rs +1 -1
  11. data/ext/kobako/src/runtime/config.rs +3 -4
  12. data/ext/kobako/src/runtime/dispatch.rs +6 -6
  13. data/ext/kobako/src/runtime/exports.rs +1 -1
  14. data/ext/kobako/src/runtime/instance_pre.rs +1 -1
  15. data/ext/kobako/src/runtime/invocation.rs +27 -27
  16. data/ext/kobako/src/runtime/trap.rs +5 -5
  17. data/ext/kobako/src/runtime.rs +44 -45
  18. data/ext/kobako/src/snapshot.rs +2 -2
  19. data/lib/kobako/capture.rb +5 -7
  20. data/lib/kobako/catalog/handles.rb +24 -31
  21. data/lib/kobako/catalog/namespaces.rb +19 -27
  22. data/lib/kobako/catalog/snippets.rb +10 -16
  23. data/lib/kobako/codec/utils.rb +6 -9
  24. data/lib/kobako/errors.rb +33 -39
  25. data/lib/kobako/handle.rb +2 -3
  26. data/lib/kobako/namespace.rb +8 -11
  27. data/lib/kobako/outcome.rb +12 -14
  28. data/lib/kobako/pool.rb +18 -24
  29. data/lib/kobako/sandbox.rb +61 -83
  30. data/lib/kobako/sandbox_options.rb +5 -9
  31. data/lib/kobako/snapshot.rb +2 -4
  32. data/lib/kobako/snippet/binary.rb +1 -3
  33. data/lib/kobako/snippet/source.rb +1 -2
  34. data/lib/kobako/snippet.rb +1 -2
  35. data/lib/kobako/transport/dispatcher.rb +39 -38
  36. data/lib/kobako/transport/request.rb +1 -1
  37. data/lib/kobako/transport/run.rb +23 -28
  38. data/lib/kobako/transport/yielder.rb +11 -17
  39. data/lib/kobako/transport.rb +2 -3
  40. data/lib/kobako/usage.rb +10 -13
  41. data/lib/kobako/version.rb +1 -1
  42. data/sig/kobako/transport/dispatcher.rbs +2 -0
  43. metadata +1 -1
@@ -7,8 +7,8 @@
7
7
  // constructed via `Kobako::Runtime.from_path(path, timeout, memory_limit,
8
8
  // stdout_limit, stderr_limit)`. Every invocation (`#eval` / `#run`)
9
9
  // instantiates a fresh instance from the InstancePre and discards the
10
- // whole Store afterwards — the docs/behavior.md B-49 per-invocation
11
- // instance discipline (ABI v2). The underlying wasmtime Engine and
10
+ // whole Store afterwards — the per-invocation instance discipline
11
+ // (ABI v2). The underlying wasmtime Engine and
12
12
  // compiled Module live in a process-scope cache (see the `cache`
13
13
  // submodule) and never surface to Ruby (SPEC.md "Code Organization":
14
14
  // `ext/` "exposes no Wasm engine types to the Host App or downstream
@@ -74,9 +74,10 @@ use self::invocation::Invocation;
74
74
 
75
75
  /// The wire ABI version this host implements (docs/wire-codec.md § ABI
76
76
  /// Version). A Guest Binary is accepted only when its
77
- /// `__kobako_abi_version` export reports the same value (B-40 / E-42);
78
- /// the guest-side mirror is `kobako_core::abi::ABI_VERSION`. Version 2
79
- /// carries the per-invocation instance discipline (B-49): the host
77
+ /// `__kobako_abi_version` export reports the same value; a mismatch
78
+ /// is a deterministic artifact fault. The guest-side mirror is
79
+ /// `kobako_core::abi::ABI_VERSION`. Version 2
80
+ /// carries the per-invocation instance discipline: the host
80
81
  /// drives every invocation on a fresh instance, so the guest may leave
81
82
  /// its VM state dirty at exit.
82
83
  const ABI_VERSION: u32 = 2;
@@ -149,22 +150,22 @@ pub(crate) fn trap_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
149
150
  /// Construct a `Kobako::SetupError` magnus error. Used for every
150
151
  /// construction-time failure on the `Runtime.from_path` path before any
151
152
  /// invocation runs — unreadable artifact, bytes that are not a valid Wasm
152
- /// module, or engine / linker / instantiation setup failure (docs/behavior.md
153
- /// E-41). The `ModuleNotBuiltError` subclass (artifact absent, E-40) is
153
+ /// module, or engine / linker / instantiation setup failure. The
154
+ /// `ModuleNotBuiltError` subclass (artifact absent) is
154
155
  /// raised through `MODULE_NOT_BUILT_ERROR` directly.
155
156
  pub(crate) fn setup_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
156
157
  error_in(ruby, &SETUP_ERROR, msg)
157
158
  }
158
159
 
159
160
  /// Construct a `Kobako::TimeoutError` magnus error. Surfaces the
160
- /// docs/behavior.md E-19 wall-clock cap path with the verb prefix added
161
+ /// wall-clock cap path with the verb prefix added
161
162
  /// by `Kobako::Sandbox#invoke!`.
162
163
  pub(crate) fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
163
164
  error_in(ruby, &TIMEOUT_ERROR, msg)
164
165
  }
165
166
 
166
167
  /// Construct a `Kobako::MemoryLimitError` magnus error. Surfaces the
167
- /// docs/behavior.md E-20 linear-memory cap path with the verb prefix
168
+ /// linear-memory cap path with the verb prefix
168
169
  /// added by `Kobako::Sandbox#invoke!`.
169
170
  pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
170
171
  error_in(ruby, &MEMORY_LIMIT_ERROR, msg)
@@ -173,8 +174,8 @@ pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusErr
173
174
  /// Construct a `Kobako::SandboxError` magnus error. Used for the
174
175
  /// host-side pre-call faults the SPEC attributes to the sandbox / wire
175
176
  /// layer rather than the Wasm engine — currently the `#run` invocation
176
- /// envelope reservation failure (`__kobako_alloc` returns 0,
177
- /// docs/behavior.md E-31). The runtime is intact, so this must not be a
177
+ /// envelope reservation failure (`__kobako_alloc` returns 0).
178
+ /// The runtime is intact, so this must not be a
178
179
  /// `TrapError`: no discard-and-recreate recovery is owed to the caller.
179
180
  pub(crate) fn sandbox_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
180
181
  error_in(ruby, &SANDBOX_ERROR, msg)
@@ -212,10 +213,9 @@ pub(crate) struct Runtime {
212
213
  // Pre-linked instantiation template (import wiring + type checks
213
214
  // done once in `instance_pre::cached_instance_pre`). Every
214
215
  // invocation instantiates a fresh instance from it and discards the
215
- // whole Store afterwards — the docs/behavior.md B-49 per-invocation
216
- // instance discipline.
216
+ // whole Store afterwards — the per-invocation instance discipline.
217
217
  instance_pre: WtInstancePre<Invocation>,
218
- // Per-invocation linear-memory cap (docs/behavior.md B-01 / E-20),
218
+ // Per-invocation linear-memory cap,
219
219
  // threaded into each fresh `Invocation`; lives apart from `Config`
220
220
  // because the wasmtime `ResourceLimiter` callback consumes it from
221
221
  // inside the wasm engine.
@@ -223,7 +223,7 @@ pub(crate) struct Runtime {
223
223
  // Wall-clock + per-channel capture caps forwarded from the Sandbox;
224
224
  // see `Config`.
225
225
  config: Config,
226
- // The host-side dispatch Proc (docs/behavior.md B-12), held here only
226
+ // The host-side dispatch Proc, held here only
227
227
  // to give `DataTypeFunctions::mark` a read path so it can pin the
228
228
  // Proc across GC. The copy the `__kobako_dispatch` import actually
229
229
  // calls is bound onto each per-invocation `Invocation` by
@@ -231,7 +231,7 @@ pub(crate) struct Runtime {
231
231
  // pinned Proc. `Cell` is sound under the GVL (see the `unsafe impl
232
232
  // Sync` below).
233
233
  on_dispatch: Cell<Option<Opaque<Value>>>,
234
- // docs/behavior.md B-35 usage of the most recent invocation —
234
+ // Usage of the most recent invocation —
235
235
  // `(wall_time_seconds, memory_peak_bytes)` — captured by
236
236
  // `build_snapshot` before the per-invocation Store is discarded so
237
237
  // `#usage` reads survive the teardown. `(0.0, 0)` before the first
@@ -245,7 +245,7 @@ impl DataTypeFunctions for Runtime {
245
245
  /// copy on `Invocation` for the duration of a guest invocation.
246
246
  /// `gc::Marker::mark` maps to `rb_gc_mark`, which pins: required because
247
247
  /// the Invocation copy is a cached `VALUE` that compaction would
248
- /// otherwise leave dangling (docs/behavior.md B-12 / B-13). Without
248
+ /// otherwise leave dangling. Without
249
249
  /// this the Proc has no GC root at all — sweep collects it (SIGSEGV on
250
250
  /// the next dispatch) and compaction relocates it (dispatch lands on
251
251
  /// the wrong receiver).
@@ -270,10 +270,10 @@ impl Runtime {
270
270
  /// Ruby-facing constructor for `Kobako::Runtime` — Engine and Module
271
271
  /// are never visible to Ruby.
272
272
  ///
273
- /// `timeout_seconds` is the docs/behavior.md B-01 wall-clock cap in seconds
273
+ /// `timeout_seconds` is the wall-clock cap in seconds
274
274
  /// (`None` disables); `memory_limit` is the linear-memory cap in
275
275
  /// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
276
- /// are the per-channel output caps (docs/behavior.md B-01 / B-04; `None`
276
+ /// are the per-channel output caps (`None`
277
277
  /// disables). All four are validated by the caller
278
278
  /// (`Kobako::Sandbox`); this method only refuses non-finite or
279
279
  /// non-positive timeouts as a defence in depth.
@@ -289,7 +289,7 @@ impl Runtime {
289
289
  None => None,
290
290
  Some(secs) if secs.is_finite() && secs > 0.0 => Some(Duration::from_secs_f64(secs)),
291
291
  Some(secs) => {
292
- // docs/behavior.md E-39: an invalid cap argument is a Host App
292
+ // An invalid cap argument is a Host App
293
293
  // programming error and raises `ArgumentError`, outside the
294
294
  // construction-failure `SetupError` branch. `SandboxOptions`
295
295
  // is the primary guard (it never lets a bad timeout reach
@@ -318,12 +318,12 @@ impl Runtime {
318
318
 
319
319
  /// Instantiate a throwaway probe instance at construction and require
320
320
  /// the guest's `__kobako_abi_version` export to equal `ABI_VERSION`
321
- /// (docs/behavior.md B-40). An absent export or a non-equal value is
322
- /// E-42 — a deterministic artifact fault raised as
321
+ /// An absent export or a non-equal value is
322
+ /// a deterministic artifact fault raised as
323
323
  /// `Kobako::SetupError`. The probe Store drops here; invocation
324
324
  /// instances are created per `#eval` / `#run`. The frameless WASI
325
325
  /// context keeps a third-party guest whose start section touches
326
- /// WASI on the E-41 `SetupError` path instead of panicking in
326
+ /// WASI on the `SetupError` path instead of panicking in
327
327
  /// `Invocation::wasi_mut`.
328
328
  fn probe_abi_version(&self, ruby: &Ruby) -> Result<(), MagnusError> {
329
329
  let mut store = self.new_store()?;
@@ -362,7 +362,7 @@ impl Runtime {
362
362
  Ok(())
363
363
  }
364
364
 
365
- /// Register the Ruby-side dispatch `Proc` (docs/behavior.md B-12).
365
+ /// Register the Ruby-side dispatch `Proc`.
366
366
  /// Bound to Ruby as `Kobako::Runtime#on_dispatch=`. The handle is
367
367
  /// pinned by `DataTypeFunctions::mark` and copied onto every
368
368
  /// per-invocation `Invocation` by `Runtime::new_store`, where the
@@ -374,7 +374,7 @@ impl Runtime {
374
374
 
375
375
  /// Synchronously re-enter the guest's `__kobako_yield_to_block`
376
376
  /// export with `args_bytes` as the yield-arguments payload, and
377
- /// return the YieldResponse bytes the guest produced (B-24).
377
+ /// return the YieldResponse bytes the guest produced.
378
378
  ///
379
379
  /// Bound to Ruby as `Kobako::Runtime#yield_to_active_invocation`.
380
380
  /// Recovers the dispatcher's `&mut Caller` from the per-thread
@@ -382,7 +382,7 @@ impl Runtime {
382
382
  /// already inside a `__kobako_dispatch` callback, so the Caller
383
383
  /// parked on the Rust stack is the same one the Sandbox-level
384
384
  /// `#eval` / `#run` is driving. Invoked from the host-side yield
385
- /// proxy that the dispatcher hands to Service methods (B-23 / B-24);
385
+ /// proxy that the dispatcher hands to Service methods;
386
386
  /// raises `Kobako::TrapError` when called outside an active dispatch
387
387
  /// frame, or when any of the underlying allocation / write / call /
388
388
  /// read steps fails.
@@ -417,10 +417,10 @@ impl Runtime {
417
417
  /// Execute one guest invocation (`__kobako_eval` — one-shot source)
418
418
  /// and return a `Snapshot` bundling every per-invocation observable.
419
419
  ///
420
- /// Builds a fresh Store + instance (B-49) whose WASI context carries
420
+ /// Builds a fresh Store + instance whose WASI context carries
421
421
  /// the three-frame stdin protocol (`preamble`, `source`, `snippets`
422
422
  /// — docs/wire-codec.md § Invocation channels), then invokes
423
- /// `__kobako_eval`. Per-invocation caps (docs/behavior.md B-01) are
423
+ /// `__kobako_eval`. Per-invocation caps are
424
424
  /// primed here: the wall-clock deadline is stamped into `Invocation`
425
425
  /// and the epoch deadline is set to fire at the next ticker tick;
426
426
  /// the memory-cap limiter is already wired.
@@ -429,7 +429,7 @@ impl Runtime {
429
429
  /// `Kobako::TimeoutError` / `Kobako::MemoryLimitError`; everything
430
430
  /// else raises `Kobako::TrapError`. On success the Snapshot carries
431
431
  /// the OUTCOME_BUFFER bytes, the per-channel stdout / stderr captures
432
- /// with their truncation flags, and the B-35 usage figures.
432
+ /// with their truncation flags, and the usage figures.
433
433
  pub(crate) fn eval(
434
434
  &self,
435
435
  preamble: RString,
@@ -457,14 +457,13 @@ impl Runtime {
457
457
  /// Execute one entrypoint dispatch (`__kobako_run`) and return a
458
458
  /// `Snapshot` bundling every per-invocation observable.
459
459
  ///
460
- /// Builds a fresh Store + instance (B-49) whose WASI context carries
460
+ /// Builds a fresh Store + instance whose WASI context carries
461
461
  /// the two-frame stdin protocol (preamble + snippets; no user source
462
462
  /// frame — docs/wire-codec.md § Invocation channels), copies
463
463
  /// `envelope` bytes into guest linear memory via `__kobako_alloc`,
464
464
  /// and calls `__kobako_run(env_ptr, env_len)`. Per-invocation cap
465
465
  /// semantics match `Runtime::eval`. Raises `Kobako::TrapError`
466
- /// ("alloc returned 0") when guest allocation fails
467
- /// (docs/behavior.md E-31).
466
+ /// ("alloc returned 0") when guest allocation fails.
468
467
  pub(crate) fn run(
469
468
  &self,
470
469
  preamble: RString,
@@ -486,7 +485,7 @@ impl Runtime {
486
485
  self.build_snapshot(&ruby, &mut store, &exports)
487
486
  }
488
487
 
489
- /// Return the docs/behavior.md B-35 per-last-invocation usage as a
488
+ /// Return the per-last-invocation usage as a
490
489
  /// Ruby 2-tuple `[wall_time, memory_peak]`. The element order
491
490
  /// matches the `Kobako::Usage` field order declared in
492
491
  /// `lib/kobako/usage.rb`; reorder both sides together if the field
@@ -520,7 +519,7 @@ impl Runtime {
520
519
 
521
520
  /// Build the per-invocation Store: a fresh `Invocation` wired with
522
521
  /// the memory limiter, the epoch-deadline callback, and the
523
- /// registered dispatch Proc (docs/behavior.md B-12).
522
+ /// registered dispatch Proc.
524
523
  fn new_store(&self) -> Result<WtStore<Invocation>, MagnusError> {
525
524
  let mut store = WtStore::new(shared_engine()?, Invocation::new(self.memory_limit));
526
525
  store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
@@ -532,10 +531,10 @@ impl Runtime {
532
531
  }
533
532
 
534
533
  /// Instantiate the per-invocation instance from the pre-linked
535
- /// template (B-49) and resolve its host-driven export handles. An
534
+ /// template and resolve its host-driven export handles. An
536
535
  /// instantiation failure at invocation time is an engine fault —
537
536
  /// `Kobako::TrapError` — unlike the construction-time probe, whose
538
- /// failure is E-41 `SetupError`.
537
+ /// failure is `SetupError`.
539
538
  fn instantiate(
540
539
  &self,
541
540
  ruby: &Ruby,
@@ -557,7 +556,7 @@ impl Runtime {
557
556
  /// `Runtime::prime_caps` before, `disarm_caps` after — the shared
558
557
  /// bracket for both run-path exports (`__kobako_eval` /
559
558
  /// `__kobako_run`). Disarm runs whether the call returns or traps, so
560
- /// the docs/behavior.md B-35 `wall_time` bracket and the E-20 memory
559
+ /// the `wall_time` bracket and the memory
561
560
  /// cap always close — that close-on-trap guarantee is the reason this
562
561
  /// bracket lives in one place rather than inline at each call site.
563
562
  /// The wasmtime trap is returned unmapped; each caller wraps it
@@ -576,7 +575,7 @@ impl Runtime {
576
575
  self.prime_caps(store, exports);
577
576
  let result = export.call(store.as_context_mut(), params);
578
577
  disarm_caps(store);
579
- // Stash the B-35 usage figures on every outcome — including the
578
+ // Stash the usage figures on every outcome — including the
580
579
  // trap paths, where `build_snapshot` never runs and the Store is
581
580
  // about to be discarded with the error.
582
581
  let data = store.data();
@@ -592,10 +591,10 @@ impl Runtime {
592
591
  /// effectively never fires.
593
592
  ///
594
593
  /// Also captures the current linear-memory size as the baseline
595
- /// for the docs/behavior.md E-20 per-invocation memory delta cap —
594
+ /// for the per-invocation memory delta cap —
596
595
  /// the pre-initialized image's allocation is folded into the
597
596
  /// baseline rather than the budget — and stamps the wall-clock
598
- /// entry instant for the docs/behavior.md B-35 `wall_time`
597
+ /// entry instant for the `wall_time`
599
598
  /// measurement. The bracket closes in `disarm_caps` so it matches
600
599
  /// the `timeout` deadline window and excludes `OUTCOME_BUFFER`
601
600
  /// decoding and stdout / stderr capture readout.
@@ -623,7 +622,7 @@ impl Runtime {
623
622
  /// Called from the run-path methods after the guest export returns
624
623
  /// successfully: drains OUTCOME_BUFFER via `__kobako_take_outcome`
625
624
  /// and snapshots the per-channel stdout / stderr pipes (clipped to
626
- /// their caps). The B-35 usage figures were already stashed by
625
+ /// their caps). The usage figures were already stashed by
627
626
  /// `call_with_caps`.
628
627
  fn build_snapshot(
629
628
  &self,
@@ -660,7 +659,7 @@ impl Runtime {
660
659
  /// Drop the memory cap as soon as the guest call returns so that
661
660
  /// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
662
661
  /// which can grow guest memory transiently) is not attributed to
663
- /// the user script. Also closes the docs/behavior.md B-35
662
+ /// the user script. Also closes the
664
663
  /// `wall_time` bracket opened by `Runtime::prime_caps`. Paired
665
664
  /// with `Runtime::prime_caps`.
666
665
  fn disarm_caps(store: &mut WtStore<Invocation>) {
@@ -683,8 +682,8 @@ fn require_memory(ruby: &Ruby, exports: &Exports) -> Result<Memory, MagnusError>
683
682
  /// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
684
683
  /// Raises `Kobako::TrapError` when the allocation hook is missing or
685
684
  /// itself traps, and `Kobako::SandboxError` when the hook runs but
686
- /// cannot reserve the buffer (`__kobako_alloc` returns 0,
687
- /// docs/behavior.md E-31) — an intact runtime, not an engine fault.
685
+ /// cannot reserve the buffer (`__kobako_alloc` returns 0) — an
686
+ /// intact runtime, not an engine fault.
688
687
  fn write_envelope(
689
688
  ruby: &Ruby,
690
689
  store: &mut WtStore<Invocation>,
@@ -723,7 +722,7 @@ fn write_envelope(
723
722
  /// instead). Each output pipe is sized at `cap + 1` so
724
723
  /// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
725
724
  /// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
726
- /// rely on `memory_limit` (docs/behavior.md E-20) for the real ceiling.
725
+ /// rely on `memory_limit` for the real ceiling.
727
726
  /// Raises `Kobako::TrapError` when any frame exceeds the 16 MiB cap that
728
727
  /// keeps its `u32` length prefix from wrapping.
729
728
  fn install_wasi_frames(
@@ -3,8 +3,8 @@
3
3
  //! Every successful `Kobako::Runtime#eval` / `#run` returns one of these.
4
4
  //! It carries every observable the host needs to surface after a guest
5
5
  //! invocation: the OUTCOME_BUFFER bytes (`return_bytes`), the captured
6
- //! stdout / stderr byte slices with their truncation flags (B-04), and
7
- //! the wall-clock + memory-peak figures from `Kobako::Usage` (B-35).
6
+ //! stdout / stderr byte slices with their truncation flags, and
7
+ //! the wall-clock + memory-peak figures from `Kobako::Usage`.
8
8
  //!
9
9
  //! Ruby callers see the seven raw readers registered below; the helper
10
10
  //! methods that pack them into `Kobako::Capture` / `Kobako::Usage`
@@ -4,7 +4,7 @@ module Kobako
4
4
  # Host-side captured prefix of guest stdout / stderr produced during a
5
5
  # single +Kobako::Sandbox+ invocation, paired with the truncation flag
6
6
  # the WASI pipe sets when the guest wrote past the configured per-channel
7
- # cap ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
7
+ # cap.
8
8
  #
9
9
  # Immutable value object: the captured bytes and the truncation flag
10
10
  # always travel together and the instance is frozen on construction.
@@ -30,14 +30,12 @@ module Kobako
30
30
  end
31
31
 
32
32
  # Returns +true+ iff the underlying capture channel exceeded its
33
- # configured cap during the originating +Sandbox+ invocation
34
- # ({docs/behavior.md B-04}[link:../../docs/behavior.md]).
33
+ # configured cap during the originating +Sandbox+ invocation.
35
34
  def truncated? = @truncated
36
35
 
37
- # Pre-invocation sentinel ({docs/behavior.md B-05}[link:../../docs/behavior.md]).
38
- # Empty UTF-8 bytes and +truncated? == false+; reused by every fresh
39
- # +Sandbox+ and by +Sandbox+ between invocations to denote "no capture
40
- # yet".
36
+ # Pre-invocation sentinel. Empty UTF-8 bytes and +truncated? == false+;
37
+ # reused by every fresh +Sandbox+ and by +Sandbox+ between invocations
38
+ # to denote "no capture yet".
41
39
  EMPTY = new(bytes: "", truncated: false)
42
40
  end
43
41
  end
@@ -5,34 +5,29 @@ require_relative "../handle"
5
5
  module Kobako
6
6
  module Catalog
7
7
  # Host-side mapping from opaque integer Handle IDs to Ruby objects.
8
- # The table is owned by +Kobako::Sandbox+
9
- # ({docs/behavior.md B-19}[link:../../../docs/behavior.md]) and injected
8
+ # The table is owned by +Kobako::Sandbox+ and injected
10
9
  # into the per-Sandbox +Kobako::Catalog::Namespaces+ so guest→host dispatch
11
10
  # resolves Handle targets and arguments against the same table that
12
- # host→guest wire encoding allocates into
13
- # ({docs/behavior.md B-14, B-34}[link:../../../docs/behavior.md]).
11
+ # host→guest wire encoding allocates into.
14
12
  #
15
- # Lifecycle invariants ({docs/behavior.md}[link:../../../docs/behavior.md]):
13
+ # Lifecycle invariants:
16
14
  #
17
- # - {docs/behavior.md B-15}[link:../../../docs/behavior.md] Handle IDs
18
- # are allocated by a monotonically increasing counter scoped to a
19
- # single invocation. The first ID issued in an invocation is 1; ID 0
20
- # is reserved as the invalid sentinel and is never returned by
21
- # +#alloc+.
15
+ # - Handle IDs are allocated by a monotonically increasing counter
16
+ # scoped to a single invocation. The first ID issued in an
17
+ # invocation is 1; ID 0 is reserved as the invalid sentinel and is
18
+ # never returned by +#alloc+.
22
19
  #
23
- # - {docs/behavior.md B-19}[link:../../../docs/behavior.md] At every
24
- # invocation boundary (via +#reset!+), every Handle issued under the
25
- # old state becomes invalid. Reset applies uniformly regardless of
26
- # allocation source (B-14 Service return or B-34 host-injected
20
+ # - At every invocation boundary (via +#reset!+), every Handle issued
21
+ # under the old state becomes invalid. Reset applies uniformly
22
+ # regardless of allocation source (Service return or host-injected
27
23
  # argument).
28
24
  #
29
- # - {docs/behavior.md B-21}[link:../../../docs/behavior.md] The cap is
30
- # +0x7fff_ffff+ (2³¹ 1). Allocation beyond the cap raises
31
- # immediately — no silent truncation, no wrap, no ID reuse.
25
+ # - The cap is +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap
26
+ # raises immediately no silent truncation, no wrap, no ID reuse.
32
27
  class Handles
33
28
  # Build a fresh, empty table. +next_id+ is an internal seam that
34
- # sets the starting value of the monotonic counter (defaults to 1 per
35
- # B-15); tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
29
+ # sets the starting value of the monotonic counter (defaults to 1);
30
+ # tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
36
31
  # the cap-exhaustion path without 2³¹ allocations.
37
32
  def initialize(next_id: 1)
38
33
  @entries = {} # : Hash[Integer, untyped]
@@ -45,8 +40,7 @@ module Kobako
45
40
  # +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
46
41
  # +Kobako::HandlerExhaustedError+ if the next ID would exceed the
47
42
  # cap. The cap is anchored on +Kobako::Handle+ — the wire codec
48
- # and the allocator share the same invariant
49
- # ({docs/behavior.md B-21}[link:../../../docs/behavior.md]).
43
+ # and the allocator share the same invariant.
50
44
  #
51
45
  # Returning a Handle (rather than a bare Integer id) keeps the
52
46
  # allocator's output a domain entity; +Kobako::Handle.restore+
@@ -69,9 +63,8 @@ module Kobako
69
63
  @entries[id]
70
64
  end
71
65
 
72
- # Clear all entries AND reset the counter to 1. Called at the per-invocation
73
- # boundary by +Kobako::Sandbox+ see
74
- # {docs/behavior.md B-19}[link:../../../docs/behavior.md]. Returns +self+.
66
+ # Clear all entries AND reset the counter to 1. Called at the
67
+ # per-invocation boundary by +Kobako::Sandbox+. Returns +self+.
75
68
  def reset!
76
69
  @entries.clear
77
70
  @next_id = 1
@@ -89,12 +82,12 @@ module Kobako
89
82
 
90
83
  private
91
84
 
92
- # Refuse to mint a Capability Handle for a reflective gadget
93
- # ({docs/behavior.md B-43}[link:../../../docs/behavior.md]): a +Binding+ /
94
- # +Method+ / +UnboundMethod+ would hand the guest a callable proxy onto
95
- # host reflection (a returned +Binding+ reaches +Binding#eval+). Raising
96
- # here keeps the rule at the single mint point, so it holds on both the
97
- # Service-return (B-14) and the +#run+ host→guest auto-wrap (B-34) paths.
85
+ # Refuse to mint a Capability Handle for a reflective gadget:
86
+ # a +Binding+ / +Method+ / +UnboundMethod+ would hand the guest a
87
+ # callable proxy onto host reflection (a returned +Binding+ reaches
88
+ # +Binding#eval+). Raising here keeps the rule at the single mint
89
+ # point, so it holds on both the Service-return and the +#run+
90
+ # host→guest auto-wrap paths.
98
91
  def reject_unwrappable!(object)
99
92
  case object
100
93
  when Binding, Method, UnboundMethod
@@ -102,7 +95,7 @@ module Kobako
102
95
  end
103
96
  end
104
97
 
105
- # Guard {#alloc} against issuing an ID past the B-21 cap. Returns +nil+
98
+ # Guard {#alloc} against issuing an ID past the cap. Returns +nil+
106
99
  # on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
107
100
  def ensure_capacity!
108
101
  cap = Kobako::Handle::MAX_ID
@@ -9,8 +9,7 @@ module Kobako
9
9
  module Catalog
10
10
  # Kobako::Catalog::Namespaces — per-Sandbox registry of
11
11
  # +Kobako::Namespace+ entities. Holds the Namespace / Member bindings
12
- # and the preamble emitted on Frame 1
13
- # ({docs/behavior.md B-07..B-11}[link:../../../docs/behavior.md]).
12
+ # and the preamble emitted on Frame 1.
14
13
  #
15
14
  # Public API:
16
15
  #
@@ -24,14 +23,13 @@ module Kobako
24
23
  # +Kobako::Transport::Dispatcher+'s responsibility — the Dispatcher
25
24
  # receives this registry and the +Catalog::Handles+ as arguments from
26
25
  # the +Runtime#on_dispatch+ Proc that +Kobako::Sandbox#initialize+
27
- # installs ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
28
- # The registry holds an injected +Catalog::Handles+ reference so
26
+ # installs. The registry holds an injected +Catalog::Handles+ reference so
29
27
  # dispatch target resolution and host→guest auto-wrap share the same
30
- # Sandbox-owned allocator ({docs/behavior.md B-19}[link:../../../docs/behavior.md]).
28
+ # Sandbox-owned allocator.
31
29
  class Namespaces
32
30
  # Build a fresh registry. +handler+ is an internal seam that injects
33
31
  # a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
34
- # is pinned near +MAX_ID+ to exercise the B-21 cap-exhaustion path
32
+ # is pinned near +MAX_ID+ to exercise the cap-exhaustion path
35
33
  # without 2³¹ allocations. Production callers leave it at the default.
36
34
  def initialize(handler: Catalog::Handles.new)
37
35
  @namespaces = {} # : Hash[String, Kobako::Namespace]
@@ -40,14 +38,12 @@ module Kobako
40
38
  @encoded = nil # : String?
41
39
  end
42
40
 
43
- # Declare or retrieve the Namespace named +name+ (idempotent
44
- # {docs/behavior.md B-10}[link:../../../docs/behavior.md]).
41
+ # Declare or retrieve the Namespace named +name+ (idempotent).
45
42
  # +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
46
43
  # +Namespace::NAME_PATTERN+). Returns the +Kobako::Namespace+ for that
47
44
  # name, creating it if it does not exist. Raises +ArgumentError+ when
48
45
  # +name+ is malformed, or when called after the owning Sandbox has been
49
- # sealed by its first invocation
50
- # ({docs/behavior.md B-07}[link:../../../docs/behavior.md]).
46
+ # sealed by its first invocation.
51
47
  def define(name)
52
48
  raise ArgumentError, "cannot define after first Sandbox invocation" if @sealed
53
49
 
@@ -74,20 +70,18 @@ module Kobako
74
70
  namespace.fetch(member_name)
75
71
  end
76
72
 
77
- # Encode the preamble as msgpack bytes for stdin Frame 1 delivery
78
- # ({docs/behavior.md B-02}[link:../../../docs/behavior.md]). Routes through
79
- # {Kobako::Codec::Encoder} like every other host-side wire encode so
80
- # there is a single codec path; the preamble carries only Strings and
81
- # Arrays, so none of the kobako ext types actually fire. Structure:
82
- # +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
83
- # +String+ of msgpack bytes.
73
+ # Encode the preamble as msgpack bytes for stdin Frame 1 delivery.
74
+ # Routes through {Kobako::Codec::Encoder} like every other host-side
75
+ # wire encode so there is a single codec path; the preamble carries
76
+ # only Strings and Arrays, so none of the kobako ext types actually
77
+ # fire. Structure: +[["Namespace", ["MemberA", "MemberB"]], ...]+.
78
+ # Returns a binary +String+ of msgpack bytes.
84
79
  #
85
80
  # Once sealed, the bytes are computed once and reused for every
86
- # subsequent invocation: B-33 seals Service registration (B-07 /
87
- # B-08) at the first invocation, so the preamble is exactly the
88
- # bindings that existed at that moment — a bind reaching a
89
- # +Kobako::Namespace+ after the seal raises +ArgumentError+ (E-45)
90
- # and never alters Frame 1.
81
+ # subsequent invocation: sealing freezes Service registration at the
82
+ # first invocation, so the preamble is exactly the bindings that
83
+ # existed at that moment — a bind reaching a +Kobako::Namespace+
84
+ # after the seal raises +ArgumentError+ and never alters Frame 1.
91
85
  def encode
92
86
  return @encoded if @encoded
93
87
 
@@ -97,11 +91,9 @@ module Kobako
97
91
  end
98
92
 
99
93
  # Mark the registry as sealed and propagate the seal to every
100
- # declared +Kobako::Namespace+
101
- # ({docs/behavior.md B-33}[link:../../../docs/behavior.md]). Called
102
- # by +Sandbox+ on the first invocation. After sealing, #define
103
- # raises ArgumentError (E-18) and +Namespace#bind+ raises
104
- # ArgumentError (E-45). Idempotent.
94
+ # declared +Kobako::Namespace+. Called by +Sandbox+ on the first
95
+ # invocation. After sealing, both #define and +Namespace#bind+
96
+ # raise ArgumentError. Idempotent.
105
97
  def seal!
106
98
  return self if @sealed
107
99
 
@@ -6,8 +6,7 @@ require_relative "../snippet"
6
6
  module Kobako
7
7
  module Catalog
8
8
  # Kobako::Catalog::Snippets — per-Sandbox insertion-ordered registry
9
- # of preloaded snippets
10
- # ({docs/behavior.md B-32 / B-33}[link:../../../docs/behavior.md]).
9
+ # of preloaded snippets.
11
10
  #
12
11
  # Entries replay against the fresh +mrb_state+ before per-invocation
13
12
  # source / entrypoint resolution. Each +Snippet::Source+ entry's +name+
@@ -15,18 +14,16 @@ module Kobako
15
14
  # +debug_info+ that surfaces in every backtrace frame originating from
16
15
  # the snippet as +(snippet:Name):line+. Duplicate names within the
17
16
  # +code:+ form would produce ambiguous attribution and are rejected at
18
- # registration time
19
- # ({docs/behavior.md E-33}[link:../../../docs/behavior.md]).
17
+ # registration time.
20
18
  # +Snippet::Binary+ entries carry no host-side name — their canonical
21
19
  # name lives in the bytecode's +debug_info+ and is read by the guest at
22
20
  # load time; the host does not extract it.
23
21
  #
24
- # Sealing (B-33) is governed by the owning Sandbox — the registry itself
22
+ # Sealing is governed by the owning Sandbox — the registry itself
25
23
  # is append-only and exposes no mutation API beyond +#register+; the
26
24
  # Sandbox guards +#register+ behind the seal check before delegating.
27
25
  class Snippets
28
- # Ruby constant-name pattern enforced on snippet names
29
- # ({docs/behavior.md E-34}[link:../../../docs/behavior.md]).
26
+ # Ruby constant-name pattern enforced on snippet names.
30
27
  NAME_PATTERN = /\A[A-Z]\w*\z/
31
28
 
32
29
  def initialize
@@ -45,7 +42,7 @@ module Kobako
45
42
  # self-encode.
46
43
  #
47
44
  # The bytes are memoized — the table is replayed verbatim on every
48
- # invocation after B-33 seals it, so Frame 3 never changes between
45
+ # invocation after sealing, so Frame 3 never changes between
49
46
  # encodes; {#register} drops the memo while the table is still open.
50
47
  def encode
51
48
  return @encoded if @encoded
@@ -53,8 +50,7 @@ module Kobako
53
50
  @encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
54
51
  end
55
52
 
56
- # Register one preloaded snippet in either of two forms
57
- # ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
53
+ # Register one preloaded snippet in either of two forms.
58
54
  #
59
55
  # * Source form +register(code: src, name: Name)+ — +src+ is the
60
56
  # mruby source as a String; the bytes are re-encoded as UTF-8
@@ -65,15 +61,13 @@ module Kobako
65
61
  # precompiled RITE bytecode as a String, duplicated and forced
66
62
  # to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
67
63
  # +nil+ — bytecode entries are anonymous on the host side; any
68
- # structural validation
69
- # ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
70
- # is deferred to the guest at first replay.
64
+ # structural validation is deferred to the guest at first replay.
71
65
  #
72
66
  # The two forms are mutually exclusive: shape validation lives
73
67
  # here so callers (chiefly +Kobako::Sandbox#preload+) collapse to
74
68
  # a single delegation. Raises +ArgumentError+ on mixed forms,
75
- # missing keywords, wrong types, malformed +name+ (E-34), or
76
- # duplicate +code:+ +name+ (E-33).
69
+ # missing keywords, wrong types, malformed +name+, or
70
+ # duplicate +code:+ +name+.
77
71
  def register(code: nil, name: nil, binary: nil)
78
72
  @encoded = nil
79
73
  if binary
@@ -89,7 +83,7 @@ module Kobako
89
83
 
90
84
  # Source-form register path. Delegates argument-shape checks to
91
85
  # +ensure_source_args!+ (which returns the narrowed +[code, name]+
92
- # pair), normalises +name+ to a Symbol, rejects duplicates (E-33),
86
+ # pair), normalises +name+ to a Symbol, rejects duplicates,
93
87
  # and appends the Source entry.
94
88
  def register_source!(code, name)
95
89
  code, name = ensure_source_args!(code, name)