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,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.17.0...kobako-runtime-v0.18.0) (2026-10-13)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **transport:** the stdin layout a Guest Binary reads changed within ABI 4. kobako_transport::envelope::Frames is replaced by Preamble, which is #[non_exhaustive] and built from Default; Bindings and Snippets no longer have their own decode and encode. Guest::eval and Guest::run, MrbGuest::eval and MrbGuest::run, and Runtime::invoke take a Preamble in place of Frames. kobako_core re-exports Preamble and Flag in place of Frames.
9
+ * **transport:** the ABI version is 4, so a Guest Binary built for ABI 3 is refused at Sandbox construction. kobako_core::export_guest! emits __kobako_downcall in place of __kobako_eval, __kobako_run, __kobako_yield_to_block and __kobako_take_outcome, and the host import is __kobako_upcall. kobako_core::abi::{take_outcome, write_outcome, write_reply, decode_run} are replaced by serve and answer, and the frames module is no longer public. Reply, YieldReply and Outcome gain an Unsupported arm and take their tags from the shared registry, and kobako_core::DispatchError and kobako::YieldError gain an Unsupported arm. A yield to a guest without block support raises Kobako::UnsupportedError at the Service's yield instead of failing the invocation as a trap.
10
+ * **runtime:** Runtime::invoke takes &kobako_transport::envelope::Frames in place of kobako_runtime::Frames, whose byte fields are gone. Guest::eval and Guest::run (and their MrbGuest flows) receive the decoded Frames, eval also its source; kobako_core::frames::read_frame is replaced by read_eval_frames and read_frames.
11
+ * **runtime:** kobako_runtime::snapshot::Completion::Outcome holds a decoded kobako_transport::envelope::Outcome instead of Vec<u8>; an engine decodes the result itself and reports an unframed one as
12
+ * **runtime:** kobako_runtime::runtime::Entry::Run holds { entrypoint: &str, payload: Vec<u8> } instead of { envelope: &[u8] }; a frontend passes the fields, and an engine encodes the Run envelope itself.
13
+ * **runtime:** kobako_runtime::yielder::Yielder::yield_to_block returns Result<YieldReply, Trap> instead of the encoded bytes; an engine implementing it decodes the reply itself.
14
+
15
+ ### Features
16
+
17
+ * **runtime:** compile a Preamble's snippets on an instance of their own ([303432d](https://github.com/elct9620/kobako/commit/303432dd90ed62dc378391515e68e784e1b473a4)), closes [#75](https://github.com/elct9620/kobako/issues/75)
18
+ * **transport:** carry each operation as a tag on one channel per direction ([c7d574b](https://github.com/elct9620/kobako/commit/c7d574b59a42e58632f2bdc46cc9a8e4c7ac6121)), closes [#73](https://github.com/elct9620/kobako/issues/73)
19
+ * **transport:** set an invocation up from a Preamble of tagged records ([84d4498](https://github.com/elct9620/kobako/commit/84d4498464f94d7312f26931ddd4ec43894a6654))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * keep a compile's bytecode only when the Preamble still fits it ([765db08](https://github.com/elct9620/kobako/commit/765db08ad31c8f5163794c7e144355f3237af93b))
25
+
26
+
27
+ ### Code Refactoring
28
+
29
+ * **runtime:** hand a frontend the decoded Outcome ([0da6aac](https://github.com/elct9620/kobako/commit/0da6aac0f7943d694a5a4ab9439b2d6c527ae1b0)), closes [#73](https://github.com/elct9620/kobako/issues/73)
30
+ * **runtime:** hand a frontend the decoded Yield Reply ([e80ab54](https://github.com/elct9620/kobako/commit/e80ab54e36c2a80b0f746c675f85bd1238fd4331)), closes [#73](https://github.com/elct9620/kobako/issues/73)
31
+ * **runtime:** let the ABI endpoints frame and read the Frames ([dea069a](https://github.com/elct9620/kobako/commit/dea069ac49a59c12a6f7102ed3cf2925f316cbff)), closes [#73](https://github.com/elct9620/kobako/issues/73)
32
+ * **runtime:** let the engine frame a run's entrypoint and payload ([62f9c94](https://github.com/elct9620/kobako/commit/62f9c9407ff3ce690723625bea075fa49d2dda47)), closes [#73](https://github.com/elct9620/kobako/issues/73)
33
+
3
34
  ## [0.17.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.16.0...kobako-runtime-v0.17.0) (2026-09-20)
4
35
 
5
36
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  [package]
15
15
  name = "kobako-runtime"
16
- version = "0.17.0"
16
+ version = "0.18.0"
17
17
  edition = "2021"
18
18
  description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
19
19
  license = "Apache-2.0"
@@ -27,4 +27,4 @@ categories = ["wasm", "virtualization"]
27
27
  # in-tree builds (and the Ruby gem, which ships both crates) resolving
28
28
  # locally.
29
29
  [dependencies]
30
- kobako-transport = { version = "0.17.0", path = "../kobako-transport" }
30
+ kobako-transport = { version = "0.18.0", path = "../kobako-transport" }
@@ -11,8 +11,9 @@ type, so the engine stays swappable.
11
11
  | Module | What it declares |
12
12
  |---|---|
13
13
  | `runtime` | the `Runtime` trait: one guest invocation on a fresh instance in, its observable `Snapshot` out |
14
- | `snapshot` | the per-invocation observables — `Completion` (outcome or trap), the two output `Capture`s, and resource `Usage`, uniform across success and trap |
15
- | `error` | the neutral failure channels: `Trap` (engine fault) and `SetupError` (the invocation never started) |
14
+ | `snapshot` | each invocation's `Completion` (outcome or trap), both output `Capture`s, and `Usage` |
15
+ | `error` | the neutral failure types: `Trap`, `SetupError`, and `InvokeError` over both |
16
+ | `profile` | the isolation `Profile` a runtime declares |
16
17
  | `dispatch` / `yielder` | the `DispatchHandler` and `Yielder` traits a frontend supplies for guest→host dispatch and block-yield re-entry |
17
18
 
18
19
  Engine implementations (such as `kobako-wasmtime`) implement
@@ -23,7 +24,7 @@ map the neutral types onto their own language surface.
23
24
 
24
25
  ```toml
25
26
  [dependencies]
26
- kobako-runtime = "0.17.0" # x-release-please-version
27
+ kobako-runtime = "0.18.0" # x-release-please-version
27
28
  ```
28
29
 
29
30
  ## License
@@ -1,9 +1,9 @@
1
- //! Engine-neutral host error channels, free of any frontend dependency.
1
+ //! Engine-neutral host error types, free of any frontend dependency.
2
2
  //!
3
3
  //! The run path produces these instead of constructing host-language
4
4
  //! exceptions directly; each frontend's boundary is the single place that
5
5
  //! maps them onto its own error classes (the Ruby ext does so in its error
6
- //! mapper). Keeping the channels frontend-free lets any engine
6
+ //! mapper). Keeping the types frontend-free lets any engine
7
7
  //! implementation produce them unchanged.
8
8
 
9
9
  use std::fmt;
@@ -26,11 +26,10 @@ pub enum Trap {
26
26
  }
27
27
 
28
28
  /// A failure that yields no invocation outcome. The discriminant records
29
- /// the runtime's state so a frontend can attribute the failure per SPEC:
29
+ /// the runtime's state so a frontend can attribute the failure:
30
30
  /// `ModuleNotBuilt` (the guest artifact is absent), `Dead` (the runtime
31
31
  /// could not be constructed), and `Intact` (the runtime is live but a
32
- /// host-side pre-call step failed, so no discard-and-recreate recovery is
33
- /// owed).
32
+ /// host-side pre-call step failed before the engine ran).
34
33
  ///
35
34
  /// Non-exhaustive, as `Trap`: a frontend maps each state onto its own
36
35
  /// class, and a later state must not break that match.
@@ -45,14 +44,14 @@ pub enum SetupError {
45
44
  /// A failure that prevented the invocation from starting — a pre-call
46
45
  /// engine fault (`Trap`) or a host-side setup fault (`Setup`) — unified so
47
46
  /// the run mechanics can propagate both with `?`; a frontend destructures
48
- /// it back into the two channels. Faults after the guest export starts
47
+ /// it back into the two. Faults after the guest export starts
49
48
  /// ride in `Completion::Trap` instead, so captures and usage survive them.
50
49
  ///
51
50
  /// Named for where it comes from — the `Err` of `Runtime::invoke` — so a
52
51
  /// host that also has its own `Error` in scope can hold both.
53
52
  ///
54
- /// Non-exhaustive, as its two channels are: a later way an invocation can
55
- /// fail to start must not break the frontends already destructuring it.
53
+ /// Non-exhaustive: a later way an invocation can fail to start must not
54
+ /// break the frontends already destructuring it.
56
55
  #[derive(Debug)]
57
56
  #[non_exhaustive]
58
57
  pub enum InvokeError {
@@ -96,7 +95,7 @@ impl fmt::Display for SetupError {
96
95
  }
97
96
  }
98
97
 
99
- // Both channels sit on the seam a third-party engine implements against,
98
+ // Both types sit on the seam a third-party engine implements against,
100
99
  // where a caller wants to lift them into `Box<dyn Error>` or `anyhow` with
101
100
  // `?`. `Trap` carries no source of its own, so neither reports one.
102
101
  impl std::error::Error for SetupError {}
@@ -24,6 +24,6 @@ pub mod yielder;
24
24
  pub use dispatch::DispatchHandler;
25
25
  pub use error::{InvokeError, SetupError, Trap};
26
26
  pub use profile::Profile;
27
- pub use runtime::{Entry, Frames, Runtime};
27
+ pub use runtime::{Entry, Runtime};
28
28
  pub use snapshot::{Capture, Completion, Snapshot, Usage};
29
29
  pub use yielder::Yielder;
@@ -11,7 +11,7 @@
11
11
  /// `Hermetic` is the full ambient-denial posture: ambient time and
12
12
  /// entropy denied at the WASI boundary, no filesystem / environment /
13
13
  /// network reachability, and no host import beyond the wire ABI's
14
- /// `__kobako_dispatch`. `Permissive` differs in exactly one grant —
14
+ /// `__kobako_upcall`. `Permissive` differs in exactly one grant —
15
15
  /// live ambient time and entropy at the WASI boundary. Ordering
16
16
  /// follows strength (`Permissive < Hermetic`), so a floor check is a
17
17
  /// plain comparison.
@@ -1,7 +1,7 @@
1
1
  //! Engine-neutral host runtime contract.
2
2
  //!
3
3
  //! The contract a wasm engine must satisfy to drive a kobako guest: take a
4
- //! per-invocation entry plus its stdin frames, run one invocation on a
4
+ //! per-invocation entry plus its Preamble, run one invocation on a
5
5
  //! fresh instance, and return the observable `Snapshot` — `Ok` iff the
6
6
  //! guest export ran, `Err` when the invocation never started. Nothing here
7
7
  //! mentions `magnus` or a Ruby type — a frontend supplies the dispatch
@@ -9,29 +9,31 @@
9
9
 
10
10
  use std::sync::Arc;
11
11
 
12
+ use kobako_transport::envelope::Preamble;
13
+
12
14
  use crate::dispatch::DispatchHandler;
13
15
  use crate::error::InvokeError;
14
16
  use crate::profile::Profile;
15
17
  use crate::snapshot::Snapshot;
16
18
 
17
19
  /// The per-invocation entry: a one-shot mruby source (`Eval`) or an
18
- /// entrypoint-dispatch envelope (`Run`). Both ride alongside the stdin
19
- /// `Frames`; `Run` additionally copies its envelope into guest memory.
20
+ /// entrypoint and its payload (`Run`). Both ride alongside the Sandbox's
21
+ /// `Preamble`; the runtime frames `Run` and the `Preamble` onto the wire,
22
+ /// so a frontend never encodes either. The payload is owned so that framing it needs no
23
+ /// second copy.
20
24
  ///
21
- /// Closed rather than `#[non_exhaustive]`: the arms are the ABI's
22
- /// invocation exports, so the set only grows with an ABI version, and a
23
- /// runtime that has not implemented the new export should fail to
24
- /// compile rather than fall into a wildcard.
25
+ /// Closed rather than `#[non_exhaustive]`: each arm is an invocation the
26
+ /// downcall carries under its own tag, and a runtime that has not
27
+ /// learned a new one should fail to compile rather than fall into a
28
+ /// wildcard.
25
29
  pub enum Entry<'a> {
26
- Eval { source: &'a [u8] },
27
- Run { envelope: &'a [u8] },
28
- }
29
-
30
- /// The stdin frames shared by both entries: the Frame 1 preamble (the
31
- /// Sandbox's registrations) and the Frame 3 snippet-replay payload.
32
- pub struct Frames<'a> {
33
- pub preamble: &'a [u8],
34
- pub snippets: &'a [u8],
30
+ Eval {
31
+ source: &'a [u8],
32
+ },
33
+ Run {
34
+ entrypoint: &'a str,
35
+ payload: Vec<u8>,
36
+ },
35
37
  }
36
38
 
37
39
  /// Engine-neutral runtime: drives one guest invocation on a fresh instance
@@ -50,7 +52,7 @@ pub trait Runtime {
50
52
  fn invoke(
51
53
  &self,
52
54
  entry: Entry<'_>,
53
- frames: Frames<'_>,
55
+ preamble: &Preamble,
54
56
  handler: Option<Arc<dyn DispatchHandler>>,
55
57
  ) -> Result<Snapshot, InvokeError>;
56
58
 
@@ -59,4 +61,16 @@ pub trait Runtime {
59
61
  /// and a frontend refuses construction when the declaration falls
60
62
  /// below the floor its host application requested.
61
63
  fn profile(&self) -> Profile;
64
+
65
+ /// The bytecode of the Preamble's source Snippets, one per snippet in
66
+ /// order, compiled on a fresh instance that runs none of them. A
67
+ /// frontend hands over the Preamble an invocation sends and replays the
68
+ /// answer in its place, so every invocation after skips the compiler.
69
+ /// `None` whenever there is nothing to replay, an answer that would
70
+ /// carry that Preamble past the payload cap included: the frontend
71
+ /// keeps the source. The default compiles nothing, so an engine that
72
+ /// does not offer it still serves every invocation.
73
+ fn compile(&self, _preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
74
+ None
75
+ }
62
76
  }
@@ -4,13 +4,14 @@
4
4
  //! frontend type and uniform across success and trap: how the invocation
5
5
  //! completed, the two captured output channels, and the resource usage.
6
6
  //! A `Snapshot` exists iff the guest export ran — a failure to even start
7
- //! the invocation travels on the `invoke` `Err` channel instead.
7
+ //! the invocation comes back as the `Err` of `invoke` instead.
8
8
  //!
9
9
  //! `Usage` and `Capture` are shared with the tier above rather than
10
10
  //! translated at its boundary: what they carry is already frontend-free,
11
11
  //! so the Rust host SDK re-exports these types themselves.
12
12
 
13
13
  use crate::error::Trap;
14
+ use kobako_transport::envelope::Outcome;
14
15
 
15
16
  /// One captured output channel: the bytes the guest wrote (already clipped
16
17
  /// to the channel's cap) and whether the cap was reached. `Default` is the
@@ -22,17 +23,19 @@ pub struct Capture {
22
23
  pub truncated: bool,
23
24
  }
24
25
 
25
- /// How the guest invocation completed: `Outcome` carries the encoded
26
- /// Outcome envelope, still undecoded; `Trap` is an engine fault after the
27
- /// export call started, kept as a value so the rest of the `Snapshot`
28
- /// survives it.
26
+ /// How the guest invocation completed: `Outcome` is the result the guest
27
+ /// left, already framed; `Trap` is an engine fault after the export call
28
+ /// started, kept as a value so the rest of the `Snapshot` survives it.
29
29
  ///
30
- /// Closed rather than `#[non_exhaustive]`: an invocation either produced
31
- /// its final message or the engine stopped it, and there is no third way
32
- /// for one to end. A wildcard would only hide a frontend that had not
33
- /// been told otherwise.
30
+ /// A guest that returned without an Outcome the envelope can frame —
31
+ /// nothing written included — ends as a `Trap`: with no record to read,
32
+ /// there is nothing a frontend could attribute.
33
+ ///
34
+ /// Closed rather than `#[non_exhaustive]`: an invocation either returned
35
+ /// a result or failed to, and there is no third way for one to end. A
36
+ /// wildcard would only hide a frontend that had not been told otherwise.
34
37
  pub enum Completion {
35
- Outcome(Vec<u8>),
38
+ Outcome(Outcome),
36
39
  Trap(Trap),
37
40
  }
38
41
 
@@ -4,19 +4,22 @@
4
4
  //! During a guest→host dispatch, a Service method may yield to a guest
5
5
  //! block. The host drives that re-entry through a `Yielder`: it ships the
6
6
  //! yield-arguments payload into the in-flight guest and returns the
7
- //! Yield Reply bytes. What backs the re-entry — a wasmtime `Caller`, some
7
+ //! decoded Yield Reply. What backs the re-entry — a wasmtime `Caller`, some
8
8
  //! other engine handle — is the implementer's concern; the dispatch
9
9
  //! contract sees only this trait.
10
10
 
11
+ use kobako_transport::envelope::YieldReply;
12
+
11
13
  use crate::error::Trap;
12
14
 
13
15
  /// Host-initiated re-entry into the in-flight guest instance to run a
14
16
  /// yielded block.
15
17
  ///
16
- /// `yield_to_block` ships `args` to `__kobako_yield_to_block` and returns
17
- /// the raw Yield Reply bytes, or a `Trap` — surfaced through the frontend's
18
- /// trap-error mapping — when the re-entry traps, the guest returns an empty
19
- /// result, or a payload exceeds the 16 MiB cap.
18
+ /// `yield_to_block` ships `args` as a `yield` downcall and returns the
19
+ /// decoded Yield Reply, `Unsupported` arm included, or a `Trap` — surfaced
20
+ /// through the frontend's trap-error mapping — when the re-entry traps,
21
+ /// the guest returns an empty or unframeable result, or a payload exceeds
22
+ /// the 16 MiB cap.
20
23
  pub trait Yielder {
21
- fn yield_to_block(&mut self, args: &[u8]) -> Result<Vec<u8>, Trap>;
24
+ fn yield_to_block(&mut self, args: &[u8]) -> Result<YieldReply, Trap>;
22
25
  }
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.17.0...kobako-transport-v0.18.0) (2026-10-13)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **transport:** the stdin layout a Guest Binary reads changed within ABI 4. kobako_transport::envelope::Frames is replaced by Preamble, which is #[non_exhaustive] and built from Default; Bindings and Snippets no longer have their own decode and encode. Guest::eval and Guest::run, MrbGuest::eval and MrbGuest::run, and Runtime::invoke take a Preamble in place of Frames. kobako_core re-exports Preamble and Flag in place of Frames.
9
+ * **mruby:** CodecError::Unsupported is renamed to CodecError::Unserved; a codec of one's own that refuses a position returns the new name.
10
+ * **transport:** the ABI version is 4, so a Guest Binary built for ABI 3 is refused at Sandbox construction. kobako_core::export_guest! emits __kobako_downcall in place of __kobako_eval, __kobako_run, __kobako_yield_to_block and __kobako_take_outcome, and the host import is __kobako_upcall. kobako_core::abi::{take_outcome, write_outcome, write_reply, decode_run} are replaced by serve and answer, and the frames module is no longer public. Reply, YieldReply and Outcome gain an Unsupported arm and take their tags from the shared registry, and kobako_core::DispatchError and kobako::YieldError gain an Unsupported arm. A yield to a guest without block support raises Kobako::UnsupportedError at the Service's yield instead of failing the invocation as a trap.
11
+
12
+ ### Features
13
+
14
+ * **transport:** carry a compile request and its answer ([f30958e](https://github.com/elct9620/kobako/commit/f30958e7d61c95667c32cfee7d9b2d2f74550a8e)), closes [#75](https://github.com/elct9620/kobako/issues/75)
15
+ * **transport:** carry each operation as a tag on one channel per direction ([c7d574b](https://github.com/elct9620/kobako/commit/c7d574b59a42e58632f2bdc46cc9a8e4c7ac6121)), closes [#73](https://github.com/elct9620/kobako/issues/73)
16
+ * **transport:** keep an unknown request's body, and refuse tag 0x00 ([edb64df](https://github.com/elct9620/kobako/commit/edb64df8f87694d17c5b6fd71ac63329023306ec)), closes [#73](https://github.com/elct9620/kobako/issues/73)
17
+ * **transport:** name the Frames a Sandbox is set up with ([52b25aa](https://github.com/elct9620/kobako/commit/52b25aa48d321ce8ea7dc9532cfade062118c64f)), closes [#73](https://github.com/elct9620/kobako/issues/73)
18
+ * **transport:** set an invocation up from a Preamble of tagged records ([84d4498](https://github.com/elct9620/kobako/commit/84d4498464f94d7312f26931ddd4ec43894a6654))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **transport:** read only an unassigned tag as an unknown request ([fcb81b5](https://github.com/elct9620/kobako/commit/fcb81b5c38d001b729a10287443c5012281b76b6)), closes [#73](https://github.com/elct9620/kobako/issues/73)
24
+ * **transport:** size a counted list by what its bytes can hold ([cfcc9fb](https://github.com/elct9620/kobako/commit/cfcc9fb6e79ec0bd1e6bf3a15659220aab0f6914))
25
+
26
+
27
+ ### Code Refactoring
28
+
29
+ * **mruby:** name a codec's refused position Unserved ([91979f9](https://github.com/elct9620/kobako/commit/91979f9546a3fa2d2dec8c136ed6affc521ba6fb)), closes [#73](https://github.com/elct9620/kobako/issues/73)
30
+
3
31
  ## [0.17.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.16.0...kobako-transport-v0.17.0) (2026-09-20)
4
32
 
5
33
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  [package]
14
14
  name = "kobako-transport"
15
- version = "0.17.0"
15
+ version = "0.18.0"
16
16
  edition = "2021"
17
17
  description = "The kobako core envelope — the fixed host↔guest wire frame every assembly shares."
18
18
  license = "Apache-2.0"
@@ -9,22 +9,29 @@ same in every assembly. This crate is both of them, and nothing else.
9
9
 
10
10
  | Module | What it fixes |
11
11
  |---|---|
12
- | `envelope` | the **core envelope**, the outer frame each message rides in: `Call` / `Reply` / `YieldReply` for a dispatch round-trip, `Outcome` / `Panic` for how an invocation ended, `Run` / `Preamble` / `Snippets` for what an invocation is handed, and the `ErrorRecord` a guest failure carries |
13
- | `abi` | the values a host and a guest must already agree on to exchange a byte: the version, the packed `(ptr, len)` return layout, the invocation-channel frame prefix, and the message size cap |
12
+ | `envelope` | the **core envelope**, the outer frame each message rides in |
13
+ | `abi` | the values both sides agree on before exchanging a byte |
14
14
 
15
- ## What the envelope does not read
15
+ The envelope covers the downcall's and the upcall's requests under one
16
+ tag registry. It also covers a dispatch round-trip, how an invocation
17
+ ended, and what an invocation is handed, and re-exports every message
18
+ type. The ABI values are the version, the packed `(ptr, len)` return
19
+ layout, the length prefix the Preamble rides stdin behind, and the
20
+ message size cap.
16
21
 
17
- The envelope reads a message's routing fields and its ok-versus-fault tag
22
+ ## Opaque Payload
23
+
24
+ The envelope reads a message's routing fields and its answer's tag
18
25
  without decoding a payload byte. Everything the resolved method consumes
19
- rides through opaque, and a decoded envelope borrows the buffer it came
20
- from, so that payload reaches its reader as a view rather than a copy.
26
+ rides through opaque. A decoded envelope borrows the buffer it came from,
27
+ so that payload reaches its reader as a view rather than a copy.
21
28
 
22
29
  ```text
23
30
  Call { method, payload }
24
31
  └── opaque here; read by whoever owns the schema
25
32
  ```
26
33
 
27
- ## Where it sits
34
+ ## Dependency Position
28
35
 
29
36
  This crate depends on no other, and every kobako tier depends on it —
30
37
  including any third-party engine, codec, or guest composing against them.
@@ -34,17 +41,19 @@ host kobako-runtime · kobako-wasmtime ──┐
34
41
  guest kobako-core · kobako-mruby ────────┴──> kobako-transport
35
42
  ```
36
43
 
37
- The byte layout is specified in
38
- [`docs/wire/envelope.md`](https://github.com/elct9620/kobako/blob/main/docs/wire/envelope.md);
39
- the golden vectors in this crate are derived from that document.
44
+ The golden vectors in this crate are derived from the kobako repository's
45
+ byte-layout document, not from this code.
40
46
 
41
47
  ## Usage
42
48
 
49
+ Depend on it directly only to build a tier of your own; a guest shell
50
+ reaches the same types through `kobako-core`.
51
+
43
52
  ```toml
44
53
  [dependencies]
45
- kobako-transport = "0.17.0" # x-release-please-version
54
+ kobako-transport = "0.18.0" # x-release-please-version
46
55
  ```
47
56
 
48
57
  ## License
49
58
 
50
- Apache-2.0
59
+ Licensed under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
@@ -1,20 +1,19 @@
1
1
  //! The ABI's fixed values — the numbers and layouts a host and a guest
2
2
  //! must already agree on before either can read the other's bytes.
3
3
  //!
4
- //! The exports and the host import that carry the ABI are declared where
5
- //! they are implemented: the guest contract crate emits the exports, the
6
- //! driver resolves them. What lives here is what both sides must spell the
4
+ //! The symbols that carry the ABI are declared where they are
5
+ //! implemented: the guest contract crate emits the exports, the driver
6
+ //! resolves them and provides the import. What lives here is what both sides must spell the
7
7
  //! same way, and each value is spelled once.
8
- //!
9
- //! [ABI signatures]: ../../../docs/wire-codec.md
10
8
 
11
9
  /// The Guest ABI version. A host accepts a Guest Binary only when the
12
- /// version it reports equals this one, so a wire change is an increment
13
- /// rather than a negotiation.
14
- pub const ABI_VERSION: u32 = 3;
10
+ /// version it reports equals this one, so a framing or symbol change is
11
+ /// an increment rather than a negotiation. A new request tag needs
12
+ /// neither, but a new answer arm is an increment.
13
+ pub const ABI_VERSION: u32 = 4;
15
14
 
16
- /// Width in bytes of the length prefix that precedes each invocation-channel
17
- /// frame and the outcome buffer.
15
+ /// Width in bytes of the length prefix that frames a message on WASI
16
+ /// stdin, such as the Preamble.
18
17
  pub const FRAME_LEN_SIZE: usize = 4;
19
18
 
20
19
  /// Single-dispatch size cap: 16 MiB in either direction, applied to the
@@ -26,16 +25,14 @@ pub const MAX_DISPATCH_PAYLOAD: usize = 16 * 1024 * 1024;
26
25
  /// length before it can check anything else about it, so this bounds what a
27
26
  /// declared length may make it allocate. It sits above
28
27
  /// `MAX_DISPATCH_PAYLOAD` because it guards the read rather than enforcing
29
- /// the contract: a frame between the two is allocated and then refused on
30
- /// its merits.
28
+ /// the contract: the host enforces the 16 MiB cap before it sends a frame.
31
29
  pub const MAX_FRAME_LEN: usize = 64 * 1024 * 1024;
32
30
 
33
31
  // The read guard must admit every frame the contract allows, so a refusal
34
32
  // names the cap rather than the allocation ceiling.
35
33
  const _: () = assert!(MAX_FRAME_LEN > MAX_DISPATCH_PAYLOAD);
36
34
 
37
- /// Pack `(ptr, len)` into the u64 the ABI's three buffer-returning
38
- /// functions answer with: the high 32 bits carry the linear-memory pointer,
35
+ /// Pack `(ptr, len)` into the u64 the downcall and the upcall answer with: the high 32 bits carry the linear-memory pointer,
39
36
  /// the low 32 the byte length. A `len` of zero is the failure signal.
40
37
  ///
41
38
  /// ```text
@@ -54,21 +54,33 @@ impl<'a> Reader<'a> {
54
54
  }
55
55
 
56
56
  /// A `u32` count followed by that many byte strings, each carrying
57
- /// UTF-8. Every list the envelope defines is a list of names, so this
58
- /// is the only list shape a reader needs.
57
+ /// UTF-8.
59
58
  ///
60
59
  /// The elements are copied rather than borrowed: a binding's path and
61
60
  /// a panic's `available` names outlive the frame buffer they arrive in.
62
61
  pub(crate) fn text_list(&mut self) -> Result<Vec<String>, DecodeError> {
62
+ self.list(|reader| Ok(reader.text()?.to_owned()))
63
+ }
64
+
65
+ /// A `u32` count followed by that many byte strings of any content,
66
+ /// copied for the same reason as `text_list`.
67
+ pub(crate) fn bytes_list(&mut self) -> Result<Vec<Vec<u8>>, DecodeError> {
68
+ self.list(|reader| Ok(reader.bytes()?.to_vec()))
69
+ }
70
+
71
+ fn list<T>(
72
+ &mut self,
73
+ mut element: impl FnMut(&mut Self) -> Result<T, DecodeError>,
74
+ ) -> Result<Vec<T>, DecodeError> {
63
75
  let count = self.u32()? as usize;
64
- // A count larger than the bytes left cannot be satisfied; refusing
65
- // it here bounds the allocation below by the message size.
66
- if count > self.remaining().len() {
76
+ // Every element opens with a `u32` length, so a count past what the
77
+ // bytes left can hold is refused before it sizes the allocation.
78
+ if count > self.remaining().len() / size_of::<u32>() {
67
79
  return Err(MALFORMED);
68
80
  }
69
81
  let mut out = Vec::with_capacity(count);
70
82
  for _ in 0..count {
71
- out.push(self.text()?.to_owned());
83
+ out.push(element(self)?);
72
84
  }
73
85
  Ok(out)
74
86
  }