kobako 0.27.0 → 0.29.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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +241 -0
  4. data/Cargo.lock +132 -126
  5. data/README.md +240 -147
  6. data/ROADMAP.md +28 -22
  7. data/SECURITY.md +2 -2
  8. data/crates/kobako-runtime/CHANGELOG.md +117 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +16 -10
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/error.rs +8 -9
  13. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  14. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  15. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  18. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  19. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  20. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  21. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  22. data/crates/kobako-runtime/src/grant.rs +20 -0
  23. data/crates/kobako-runtime/src/lib.rs +12 -2
  24. data/crates/kobako-runtime/src/mount.rs +178 -0
  25. data/crates/kobako-runtime/src/profile.rs +4 -4
  26. data/crates/kobako-runtime/src/runtime.rs +43 -17
  27. data/crates/kobako-runtime/src/snapshot.rs +33 -15
  28. data/crates/kobako-runtime/src/yielder.rs +14 -6
  29. data/crates/kobako-transport/CHANGELOG.md +45 -0
  30. data/crates/kobako-transport/Cargo.toml +1 -1
  31. data/crates/kobako-transport/README.md +21 -12
  32. data/crates/kobako-transport/src/abi.rs +11 -14
  33. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  34. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  35. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  36. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  37. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  38. data/crates/kobako-transport/src/envelope/reply.rs +103 -61
  39. data/crates/kobako-transport/src/envelope/request.rs +305 -0
  40. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  41. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  42. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  43. data/crates/kobako-transport/src/envelope.rs +19 -12
  44. data/crates/kobako-transport/src/lib.rs +0 -2
  45. data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
  46. data/crates/kobako-wasmtime/Cargo.toml +23 -16
  47. data/crates/kobako-wasmtime/README.md +6 -4
  48. data/crates/kobako-wasmtime/src/abi.rs +67 -36
  49. data/crates/kobako-wasmtime/src/ambient.rs +64 -5
  50. data/crates/kobako-wasmtime/src/cache.rs +204 -127
  51. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  52. data/crates/kobako-wasmtime/src/config.rs +69 -15
  53. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  54. data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
  55. data/crates/kobako-wasmtime/src/driver.rs +174 -125
  56. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  57. data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
  58. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  59. data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
  60. data/crates/kobako-wasmtime/src/invocation.rs +84 -412
  61. data/crates/kobako-wasmtime/src/io.rs +89 -0
  62. data/crates/kobako-wasmtime/src/lib.rs +7 -24
  63. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  64. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  65. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  66. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  67. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  68. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  69. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  70. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  71. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  72. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  73. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  74. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  75. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  76. data/crates/kobako-wasmtime/src/trap.rs +66 -59
  77. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  78. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  79. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  80. data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
  81. data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
  82. data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
  83. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  84. data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
  85. data/data/kobako.wasm +0 -0
  86. data/ext/kobako/Cargo.toml +4 -2
  87. data/ext/kobako/src/runtime/bridge.rs +177 -113
  88. data/ext/kobako/src/runtime/errors.rs +26 -32
  89. data/ext/kobako/src/runtime/escape.rs +53 -0
  90. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  91. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  92. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  93. data/ext/kobako/src/runtime/gvl.rs +11 -13
  94. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  95. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  96. data/ext/kobako/src/runtime.rs +190 -312
  97. data/lib/kobako/capture.rb +1 -4
  98. data/lib/kobako/catalog/extensions.rb +21 -59
  99. data/lib/kobako/catalog/flags.rb +29 -0
  100. data/lib/kobako/catalog/grants.rb +30 -0
  101. data/lib/kobako/catalog/handles.rb +7 -54
  102. data/lib/kobako/catalog/mount.rb +27 -0
  103. data/lib/kobako/catalog/services.rb +17 -60
  104. data/lib/kobako/catalog/snippets.rb +41 -69
  105. data/lib/kobako/catalog.rb +13 -10
  106. data/lib/kobako/codec/decoder.rb +10 -28
  107. data/lib/kobako/codec/encoder.rb +3 -22
  108. data/lib/kobako/codec/error.rb +6 -27
  109. data/lib/kobako/codec/ext_types.rb +72 -94
  110. data/lib/kobako/codec/handle_walk.rb +65 -130
  111. data/lib/kobako/codec/nesting.rb +3 -8
  112. data/lib/kobako/codec/state.rb +21 -11
  113. data/lib/kobako/codec/utils.rb +6 -34
  114. data/lib/kobako/codec/value_budget.rb +86 -0
  115. data/lib/kobako/codec.rb +10 -22
  116. data/lib/kobako/constant_name.rb +8 -0
  117. data/lib/kobako/context.rb +83 -117
  118. data/lib/kobako/errors.rb +57 -132
  119. data/lib/kobako/execution.rb +16 -21
  120. data/lib/kobako/extension.rb +23 -40
  121. data/lib/kobako/filesystem.rb +38 -0
  122. data/lib/kobako/handle.rb +7 -12
  123. data/lib/kobako/outcome.rb +51 -80
  124. data/lib/kobako/payload/arguments.rb +1 -16
  125. data/lib/kobako/payload.rb +4 -12
  126. data/lib/kobako/pool.rb +25 -55
  127. data/lib/kobako/runtime.rb +7 -21
  128. data/lib/kobako/sandbox.rb +128 -137
  129. data/lib/kobako/sandbox_options.rb +71 -90
  130. data/lib/kobako/snippet/bytecode.rb +11 -0
  131. data/lib/kobako/snippet/source.rb +1 -16
  132. data/lib/kobako/snippet.rb +2 -10
  133. data/lib/kobako/transport/block_failure.rb +35 -0
  134. data/lib/kobako/transport/call.rb +1 -14
  135. data/lib/kobako/transport/dispatcher.rb +127 -206
  136. data/lib/kobako/transport/error.rb +6 -20
  137. data/lib/kobako/transport/exposure.rb +10 -17
  138. data/lib/kobako/transport/reflection.rb +108 -63
  139. data/lib/kobako/transport/run.rb +17 -71
  140. data/lib/kobako/transport/yielder.rb +20 -94
  141. data/lib/kobako/transport.rb +4 -15
  142. data/lib/kobako/unresolved.rb +4 -11
  143. data/lib/kobako/usage.rb +2 -6
  144. data/lib/kobako/version.rb +1 -1
  145. data/lib/kobako.rb +1 -0
  146. data/sig/kobako/catalog/extensions.rbs +2 -2
  147. data/sig/kobako/catalog/flags.rbs +13 -0
  148. data/sig/kobako/catalog/grants.rbs +13 -0
  149. data/sig/kobako/catalog/mount.rbs +28 -0
  150. data/sig/kobako/catalog/services.rbs +3 -5
  151. data/sig/kobako/catalog/snippets.rbs +5 -5
  152. data/sig/kobako/catalog.rbs +1 -0
  153. data/sig/kobako/codec/ext_types.rbs +10 -8
  154. data/sig/kobako/codec/handle_walk.rbs +9 -7
  155. data/sig/kobako/codec/state.rbs +7 -1
  156. data/sig/kobako/codec/utils.rbs +2 -2
  157. data/sig/kobako/codec/value_budget.rbs +23 -0
  158. data/sig/kobako/codec.rbs +4 -2
  159. data/sig/kobako/constant_name.rbs +3 -0
  160. data/sig/kobako/context.rbs +15 -8
  161. data/sig/kobako/errors.rbs +10 -4
  162. data/sig/kobako/filesystem.rbs +88 -0
  163. data/sig/kobako/guest_yielder.rbs +9 -0
  164. data/sig/kobako/outcome.rbs +6 -8
  165. data/sig/kobako/pool.rbs +1 -1
  166. data/sig/kobako/runtime.rbs +21 -45
  167. data/sig/kobako/sandbox.rbs +11 -1
  168. data/sig/kobako/sandbox_options.rbs +25 -7
  169. data/sig/kobako/snapshot.rbs +20 -0
  170. data/sig/kobako/snippet/bytecode.rbs +12 -0
  171. data/sig/kobako/transport/block_failure.rbs +13 -0
  172. data/sig/kobako/transport/dispatcher.rbs +15 -19
  173. data/sig/kobako/transport/exposure.rbs +1 -1
  174. data/sig/kobako/transport/reflection.rbs +19 -4
  175. data/sig/kobako/transport/run.rbs +1 -3
  176. data/sig/kobako/transport/yielder.rbs +5 -9
  177. data/sig/kobako/transport.rbs +9 -8
  178. metadata +75 -10
  179. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  180. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  181. data/lib/kobako/snippet/binary.rb +0 -26
  182. data/rust-toolchain.toml +0 -15
  183. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -0,0 +1,178 @@
1
+ //! A Mount: the Grant handing a Filesystem to the guest at an absolute
2
+ //! guest path, with the access it allows there. The Mount decides each
3
+ //! open and each change itself, so a runtime asks it rather than reading
4
+ //! its access, and a Filesystem of a third party's checks nothing.
5
+
6
+ use std::fmt;
7
+ use std::sync::Arc;
8
+
9
+ use crate::filesystem::{Access, Filesystem};
10
+
11
+ /// The Host App's decision for one path a guest opens or changes, made in
12
+ /// a Mount's access's place.
13
+ pub trait Policy: Send + Sync {
14
+ /// Whether the guest may reach `path`, the absolute guest path, with
15
+ /// `access`.
16
+ fn allows(&self, path: &str, access: Access) -> bool;
17
+ }
18
+
19
+ /// Why a Mount refused a call.
20
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
21
+ pub enum Denial {
22
+ /// A change beneath a Mount allowing only reads.
23
+ ReadOnly,
24
+ /// The Mount's Policy said no.
25
+ Refused,
26
+ }
27
+
28
+ /// A Filesystem at an absolute guest path. Its path bounds what a Policy
29
+ /// can allow either way.
30
+ #[derive(Clone)]
31
+ pub struct Mount {
32
+ at: String,
33
+ access: Access,
34
+ filesystem: Arc<dyn Filesystem>,
35
+ policy: Option<Arc<dyn Policy>>,
36
+ }
37
+
38
+ /// A path no Mount can stand at: one that is not an absolute guest path.
39
+ #[derive(Clone, Debug, PartialEq, Eq)]
40
+ pub struct MountPathError(String);
41
+
42
+ impl fmt::Display for MountPathError {
43
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44
+ write!(
45
+ f,
46
+ "mount path must be an absolute guest path, got {:?}",
47
+ self.0
48
+ )
49
+ }
50
+ }
51
+
52
+ impl std::error::Error for MountPathError {}
53
+
54
+ /// An absolute guest path has no empty, `.`, or `..` component; the root
55
+ /// is `/` alone.
56
+ fn is_guest_path(at: &str) -> bool {
57
+ at == "/"
58
+ || at.strip_prefix('/').is_some_and(|rest| {
59
+ rest.split('/')
60
+ .all(|part| !part.is_empty() && part != "." && part != "..")
61
+ })
62
+ }
63
+
64
+ impl Mount {
65
+ /// A Mount of `filesystem` at `at`, refused unless `at` is an absolute
66
+ /// guest path, which every path the Mount answers for is built on.
67
+ pub fn new(
68
+ at: impl Into<String>,
69
+ filesystem: Arc<dyn Filesystem>,
70
+ access: Access,
71
+ ) -> Result<Self, MountPathError> {
72
+ let at = at.into();
73
+ if !is_guest_path(&at) {
74
+ return Err(MountPathError(at));
75
+ }
76
+ Ok(Self {
77
+ at,
78
+ access,
79
+ filesystem,
80
+ policy: None,
81
+ })
82
+ }
83
+
84
+ /// This Mount with `policy` deciding each open and each change in its
85
+ /// access's place.
86
+ pub fn with_policy(self, policy: Arc<dyn Policy>) -> Self {
87
+ Self {
88
+ policy: Some(policy),
89
+ ..self
90
+ }
91
+ }
92
+
93
+ /// The absolute guest path the Filesystem appears at.
94
+ pub fn at(&self) -> &str {
95
+ &self.at
96
+ }
97
+
98
+ /// The Filesystem an engine hands each call beneath this Mount.
99
+ pub fn filesystem(&self) -> &Arc<dyn Filesystem> {
100
+ &self.filesystem
101
+ }
102
+
103
+ /// Whether a change beneath this Mount may be allowed at all: it was made
104
+ /// to be written, or a Policy decides each change in its access's place.
105
+ /// The Policy itself is not asked.
106
+ pub fn allows_writes(&self) -> bool {
107
+ self.policy.is_some() || self.access == Access::ReadWrite
108
+ }
109
+
110
+ /// The absolute guest path `path`, relative to this Mount's root,
111
+ /// stands for.
112
+ pub fn guest_path(&self, path: &str) -> String {
113
+ match (self.at.as_str(), path) {
114
+ (at, "") => at.to_string(),
115
+ ("/", path) => format!("/{path}"),
116
+ (at, path) => format!("{at}/{path}"),
117
+ }
118
+ }
119
+
120
+ /// Whether the guest may reach `path`, relative to this Mount's root,
121
+ /// with `access`. A Policy judges the path as the Filesystem stores
122
+ /// its names, so a host folding case or Unicode form cannot hand it
123
+ /// one spelling of an entry it refuses by another; a Filesystem that
124
+ /// cannot spell it refuses.
125
+ pub fn check(&self, path: &str, access: Access) -> Result<(), Denial> {
126
+ match &self.policy {
127
+ Some(policy) => {
128
+ let stored = self
129
+ .filesystem
130
+ .stored_path(path)
131
+ .map_err(|_| Denial::Refused)?;
132
+ if policy.allows(&self.guest_path(&stored), access) {
133
+ Ok(())
134
+ } else {
135
+ Err(Denial::Refused)
136
+ }
137
+ }
138
+ None if access == Access::ReadWrite && self.access == Access::Read => {
139
+ Err(Denial::ReadOnly)
140
+ }
141
+ None => Ok(()),
142
+ }
143
+ }
144
+
145
+ /// Whether a directory may move beneath this Mount. Under a Policy it
146
+ /// may not: everything beneath it would take paths the Policy never
147
+ /// judged, so a move is refused as one across devices and the guest
148
+ /// copies entry by entry instead, as Landlock refuses such a move.
149
+ pub fn moves_directories(&self) -> bool {
150
+ self.policy.is_none()
151
+ }
152
+
153
+ /// Whether a path beneath this Mount may pass through a link the
154
+ /// Filesystem holds. Under a Policy it may not: the link leads to a
155
+ /// path the Policy never judged, so the call is refused as one past
156
+ /// too many links.
157
+ pub fn follows_links(&self) -> bool {
158
+ self.policy.is_none()
159
+ }
160
+
161
+ /// This Mount as a new Invocation starts from it, over the Filesystem
162
+ /// that Invocation receives; beneath a Policy, the one that follows no
163
+ /// link, so each call reaches the path the Policy judged.
164
+ pub fn for_invocation(&self) -> Self {
165
+ let filesystem = self
166
+ .filesystem
167
+ .for_invocation()
168
+ .unwrap_or_else(|| self.filesystem.clone());
169
+ let filesystem = match self.policy {
170
+ Some(_) => filesystem.for_policy().unwrap_or(filesystem),
171
+ None => filesystem,
172
+ };
173
+ Self {
174
+ filesystem,
175
+ ..self.clone()
176
+ }
177
+ }
178
+ }
@@ -9,10 +9,10 @@
9
9
  /// The ordered isolation ladder a runtime builds one rung of.
10
10
  ///
11
11
  /// `Hermetic` is the full ambient-denial posture: ambient time and
12
- /// entropy denied at the WASI boundary, no filesystem / environment /
13
- /// network reachability, and no host import beyond the wire ABI's
14
- /// `__kobako_dispatch`. `Permissive` differs in exactly one grant —
15
- /// live ambient time and entropy at the WASI boundary. Ordering
12
+ /// entropy denied at the WASI boundary, no ambient filesystem /
13
+ /// environment / network reachability, and no host import beyond the wire ABI's
14
+ /// `__kobako_upcall`. `Permissive` differs only in the ambient
15
+ /// authority it grants — live time and entropy at the WASI boundary. Ordering
16
16
  /// follows strength (`Permissive < Hermetic`), so a floor check is a
17
17
  /// plain comparison.
18
18
  ///
@@ -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,32 @@
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;
16
+ use crate::grant::{Grant, GrantKind};
14
17
  use crate::profile::Profile;
15
18
  use crate::snapshot::Snapshot;
16
19
 
17
20
  /// 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.
21
+ /// entrypoint and its payload (`Run`). Both ride alongside the Sandbox's
22
+ /// `Preamble`; the runtime frames `Run` and the `Preamble` onto the wire,
23
+ /// so a frontend never encodes either. The payload is owned so that framing it needs no
24
+ /// second copy.
20
25
  ///
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.
26
+ /// Closed rather than `#[non_exhaustive]`: each arm is an invocation the
27
+ /// downcall carries under its own tag, and a runtime that has not
28
+ /// learned a new one should fail to compile rather than fall into a
29
+ /// wildcard.
25
30
  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],
31
+ Eval {
32
+ source: &'a [u8],
33
+ },
34
+ Run {
35
+ entrypoint: &'a str,
36
+ payload: Vec<u8>,
37
+ },
35
38
  }
36
39
 
37
40
  /// Engine-neutral runtime: drives one guest invocation on a fresh instance
@@ -47,16 +50,39 @@ pub struct Frames<'a> {
47
50
  /// wrapper and GC-marking it; the runtime itself touches no frontend
48
51
  /// value.
49
52
  pub trait Runtime {
53
+ /// `grants` are this Invocation's, each of a kind `supports` declared;
54
+ /// the runtime presents them to the guest and keeps none afterwards.
50
55
  fn invoke(
51
56
  &self,
52
57
  entry: Entry<'_>,
53
- frames: Frames<'_>,
58
+ preamble: &Preamble,
54
59
  handler: Option<Arc<dyn DispatchHandler>>,
60
+ grants: &[Grant],
55
61
  ) -> Result<Snapshot, InvokeError>;
56
62
 
63
+ /// Whether this runtime presents a Grant of `kind` to the guest. A
64
+ /// frontend refuses to register one it does not, before any guest runs.
65
+ /// The default supports none, so an engine that has not learned Grants
66
+ /// still serves every invocation without one.
67
+ fn supports(&self, _kind: GrantKind) -> bool {
68
+ false
69
+ }
70
+
57
71
  /// The isolation profile this runtime provides. Deliberately
58
72
  /// without a default: every engine states its posture explicitly,
59
73
  /// and a frontend refuses construction when the declaration falls
60
74
  /// below the floor its host application requested.
61
75
  fn profile(&self) -> Profile;
76
+
77
+ /// The bytecode of the Preamble's source Snippets, one per snippet in
78
+ /// order, compiled on a fresh instance that runs none of them. A
79
+ /// frontend hands over the Preamble an invocation sends and replays the
80
+ /// answer in its place, so every invocation after skips the compiler.
81
+ /// `None` whenever there is nothing to replay, an answer that would
82
+ /// carry that Preamble past the payload cap included: the frontend
83
+ /// keeps the source. The default compiles nothing, so an engine that
84
+ /// does not offer it still serves every invocation.
85
+ fn compile(&self, _preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
86
+ None
87
+ }
62
88
  }
@@ -4,44 +4,49 @@
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
17
18
  /// pre-invocation sentinel — no bytes, cap not reached — shared by every
18
19
  /// frontend's before-first-invocation readout.
19
20
  #[derive(Default)]
21
+ #[non_exhaustive]
20
22
  pub struct Capture {
21
23
  pub bytes: Vec<u8>,
22
24
  pub truncated: bool,
23
25
  }
24
26
 
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.
27
+ /// How the guest invocation completed: `Outcome` is the result the guest
28
+ /// left, already framed; `Trap` is an engine fault after the export call
29
+ /// started, kept as a value so the rest of the `Snapshot` survives it.
29
30
  ///
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.
31
+ /// A guest that returned without an Outcome the envelope can frame —
32
+ /// nothing written included — ends as a `Trap`: with no record to read,
33
+ /// there is nothing a frontend could attribute.
34
+ ///
35
+ /// Closed rather than `#[non_exhaustive]`: an invocation either returned
36
+ /// a result or failed to, and there is no third way for one to end. A
37
+ /// wildcard would only hide a frontend that had not been told otherwise.
34
38
  pub enum Completion {
35
- Outcome(Vec<u8>),
39
+ Outcome(Outcome),
36
40
  Trap(Trap),
37
41
  }
38
42
 
39
- /// Resource usage of one guest invocation, measured across the same
40
- /// bracket as the wall-clock / memory caps: `wall_time` is the seconds
41
- /// spent inside the guest export call; `memory_peak` is the high-water
42
- /// `memory.grow` delta in bytes past the entry-time baseline. `Default`
43
- /// (all zeros) is the pre-invocation sentinel.
43
+ /// Resource usage of one guest invocation, each measured across the span
44
+ /// its cap governs: `wall_time` is the seconds from setting the guest up to
45
+ /// its export call returning, the span the deadline covers; `memory_peak`
46
+ /// is the high-water `memory.grow` delta in bytes past the entry-time
47
+ /// baseline. `Default` (all zeros) is the pre-invocation sentinel.
44
48
  #[derive(Clone, Copy, Default)]
49
+ #[non_exhaustive]
45
50
  pub struct Usage {
46
51
  pub wall_time: f64,
47
52
  pub memory_peak: usize,
@@ -51,9 +56,22 @@ pub struct Usage {
51
56
  /// kinds: captures and usage are collected on trap and outcome alike.
52
57
  /// What a frontend exposes from the trap path is its own contract
53
58
  /// decision.
59
+ #[non_exhaustive]
54
60
  pub struct Snapshot {
55
61
  pub completion: Completion,
56
62
  pub stdout: Capture,
57
63
  pub stderr: Capture,
58
64
  pub usage: Usage,
59
65
  }
66
+
67
+ impl Snapshot {
68
+ /// What an engine hands back from one invocation.
69
+ pub fn new(completion: Completion, stdout: Capture, stderr: Capture, usage: Usage) -> Self {
70
+ Self {
71
+ completion,
72
+ stdout,
73
+ stderr,
74
+ usage,
75
+ }
76
+ }
77
+ }
@@ -4,19 +4,27 @@
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
+ /// Each open Call has a `frame`, counted from `0` at the outermost.
19
+ /// `yield_to_block` ships `args` as a `yield` downcall to the block of the
20
+ /// open Call `frame` names, and returns the decoded Yield Reply,
21
+ /// `Unsupported` arm included, or a `Trap` — surfaced through the
22
+ /// frontend's trap-error mapping — when the re-entry traps, the guest
23
+ /// returns an empty or unframeable result, or a payload exceeds the
24
+ /// 16 MiB cap.
20
25
  pub trait Yielder {
21
- fn yield_to_block(&mut self, args: &[u8]) -> Result<Vec<u8>, Trap>;
26
+ /// The frame of the Call this Yielder was lent for.
27
+ fn frame(&self) -> u32;
28
+
29
+ fn yield_to_block(&mut self, frame: u32, args: &[u8]) -> Result<YieldReply, Trap>;
22
30
  }
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.19.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.18.0...kobako-transport-v0.19.0) (2026-10-26)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * in kobako-transport, YieldReply::Break carries no payload and Reply gains a Break arm, so a Guest Binary built against the earlier version 5 wire must be rebuilt.
9
+ * the Guest ABI is version 5, so a Guest Binary built against version 4 is refused at Sandbox construction until rebuilt. The Yielder trait of kobako-runtime gains frame and takes the frame in yield_to_block; the Guest trait of kobako-core and the MrbGuest trait of kobako-mruby take the frame in yield_to_block; Downcall::Yield carries frame and args.
10
+
11
+ ### Features
12
+
13
+ * let a block's break or return unwind a Service as a plain method ([4e9eca0](https://github.com/elct9620/kobako/commit/4e9eca05f1647c8b6409fc00d08317b431c0c648))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * run a stored block as the block of the Call that carried it ([bf33dc1](https://github.com/elct9620/kobako/commit/bf33dc1fd40c83498c6c12b0be52b3a835a144cf))
19
+
20
+ ## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.17.0...kobako-transport-v0.18.0) (2026-10-13)
21
+
22
+
23
+ ### ⚠ BREAKING CHANGES
24
+
25
+ * **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.
26
+ * **mruby:** CodecError::Unsupported is renamed to CodecError::Unserved; a codec of one's own that refuses a position returns the new name.
27
+ * **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.
28
+
29
+ ### Features
30
+
31
+ * **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)
32
+ * **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)
33
+ * **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)
34
+ * **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)
35
+ * **transport:** set an invocation up from a Preamble of tagged records ([84d4498](https://github.com/elct9620/kobako/commit/84d4498464f94d7312f26931ddd4ec43894a6654))
36
+
37
+
38
+ ### Bug Fixes
39
+
40
+ * **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)
41
+ * **transport:** size a counted list by what its bytes can hold ([cfcc9fb](https://github.com/elct9620/kobako/commit/cfcc9fb6e79ec0bd1e6bf3a15659220aab0f6914))
42
+
43
+
44
+ ### Code Refactoring
45
+
46
+ * **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)
47
+
3
48
  ## [0.17.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.16.0...kobako-transport-v0.17.0) (2026-09-20)
4
49
 
5
50
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  [package]
14
14
  name = "kobako-transport"
15
- version = "0.17.0"
15
+ version = "0.19.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.19.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 = 5;
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
  }