kobako 0.19.0 → 0.21.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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +127 -0
  4. data/Cargo.lock +15 -5
  5. data/README.md +96 -44
  6. data/ROADMAP.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +44 -0
  8. data/crates/kobako-runtime/Cargo.toml +8 -1
  9. data/crates/kobako-runtime/README.md +1 -1
  10. data/crates/kobako-runtime/src/dispatch.rs +17 -11
  11. data/crates/kobako-runtime/src/error.rs +38 -6
  12. data/crates/kobako-runtime/src/lib.rs +13 -2
  13. data/crates/kobako-runtime/src/profile.rs +8 -0
  14. data/crates/kobako-runtime/src/runtime.rs +7 -2
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -4
  16. data/crates/kobako-runtime/src/yielder.rs +4 -4
  17. data/crates/kobako-transport/CHANGELOG.md +31 -0
  18. data/crates/kobako-transport/Cargo.toml +27 -0
  19. data/crates/kobako-transport/README.md +43 -0
  20. data/crates/kobako-transport/src/abi.rs +89 -0
  21. data/crates/kobako-transport/src/envelope/bytes.rs +234 -0
  22. data/crates/kobako-transport/src/envelope/call.rs +222 -0
  23. data/crates/kobako-transport/src/envelope/error_record.rs +101 -0
  24. data/crates/kobako-transport/src/envelope/fault.rs +171 -0
  25. data/crates/kobako-transport/src/envelope/invocation_frames.rs +232 -0
  26. data/crates/kobako-transport/src/envelope/outcome.rs +261 -0
  27. data/crates/kobako-transport/src/envelope/reply.rs +240 -0
  28. data/crates/kobako-transport/src/envelope/run.rs +85 -0
  29. data/crates/kobako-transport/src/envelope.rs +73 -0
  30. data/crates/kobako-transport/src/lib.rs +21 -0
  31. data/crates/kobako-wasmtime/CHANGELOG.md +30 -0
  32. data/crates/kobako-wasmtime/Cargo.toml +5 -2
  33. data/crates/kobako-wasmtime/README.md +1 -1
  34. data/crates/kobako-wasmtime/src/config.rs +19 -12
  35. data/crates/kobako-wasmtime/src/dispatch.rs +15 -8
  36. data/crates/kobako-wasmtime/src/driver.rs +8 -27
  37. data/crates/kobako-wasmtime/src/frames.rs +11 -9
  38. data/crates/kobako-wasmtime/src/guest_mem.rs +7 -15
  39. data/crates/kobako-wasmtime/src/instance_pre.rs +2 -2
  40. data/crates/kobako-wasmtime/src/invocation.rs +1 -1
  41. data/crates/kobako-wasmtime/src/lib.rs +1 -1
  42. data/data/kobako.wasm +0 -0
  43. data/ext/kobako/Cargo.toml +7 -2
  44. data/ext/kobako/src/runtime/bridge.rs +103 -40
  45. data/ext/kobako/src/runtime/errors.rs +13 -5
  46. data/ext/kobako/src/runtime/gvl.rs +114 -0
  47. data/ext/kobako/src/runtime.rs +307 -207
  48. data/lib/kobako/catalog/extensions.rb +50 -43
  49. data/lib/kobako/catalog/handles.rb +7 -16
  50. data/lib/kobako/catalog/services.rb +24 -46
  51. data/lib/kobako/catalog/snippets.rb +16 -33
  52. data/lib/kobako/codec/decoder.rb +10 -13
  53. data/lib/kobako/codec/encoder.rb +7 -7
  54. data/lib/kobako/codec/error.rb +6 -6
  55. data/lib/kobako/codec/ext_types.rb +12 -72
  56. data/lib/kobako/codec/handle_walk.rb +6 -6
  57. data/lib/kobako/codec/state.rb +4 -56
  58. data/lib/kobako/codec/utils.rb +4 -4
  59. data/lib/kobako/codec.rb +11 -21
  60. data/lib/kobako/context.rb +208 -0
  61. data/lib/kobako/errors.rb +59 -16
  62. data/lib/kobako/execution.rb +52 -0
  63. data/lib/kobako/extension.rb +24 -10
  64. data/lib/kobako/outcome.rb +71 -128
  65. data/lib/kobako/payload/arguments.rb +59 -0
  66. data/lib/kobako/payload.rb +19 -0
  67. data/lib/kobako/pool.rb +1 -7
  68. data/lib/kobako/sandbox.rb +51 -191
  69. data/lib/kobako/sandbox_options.rb +24 -3
  70. data/lib/kobako/snippet/binary.rb +3 -4
  71. data/lib/kobako/snippet/source.rb +5 -6
  72. data/lib/kobako/snippet.rb +2 -2
  73. data/lib/kobako/transport/call.rb +21 -0
  74. data/lib/kobako/transport/dispatcher.rb +94 -142
  75. data/lib/kobako/transport/error.rb +21 -5
  76. data/lib/kobako/transport/reflection.rb +82 -0
  77. data/lib/kobako/transport/run.rb +25 -33
  78. data/lib/kobako/transport/yielder.rb +27 -36
  79. data/lib/kobako/transport.rb +11 -8
  80. data/lib/kobako/unresolved.rb +17 -0
  81. data/lib/kobako/usage.rb +7 -7
  82. data/lib/kobako/version.rb +1 -1
  83. data/lib/kobako.rb +1 -0
  84. data/release-please-config.json +47 -2
  85. data/sig/kobako/catalog/extensions.rbs +4 -6
  86. data/sig/kobako/catalog/handles.rbs +0 -2
  87. data/sig/kobako/catalog/services.rbs +4 -4
  88. data/sig/kobako/catalog/snippets.rbs +4 -2
  89. data/sig/kobako/codec/error.rbs +4 -4
  90. data/sig/kobako/codec/ext_types.rbs +0 -7
  91. data/sig/kobako/codec/state.rbs +0 -4
  92. data/sig/kobako/codec.rbs +4 -5
  93. data/sig/kobako/context.rbs +36 -0
  94. data/sig/kobako/errors.rbs +27 -6
  95. data/sig/kobako/execution.rbs +24 -0
  96. data/sig/kobako/extension.rbs +4 -2
  97. data/sig/kobako/outcome.rbs +12 -13
  98. data/sig/kobako/payload/arguments.rbs +26 -0
  99. data/sig/kobako/payload.rbs +4 -0
  100. data/sig/kobako/pool.rbs +0 -2
  101. data/sig/kobako/runtime.rbs +44 -14
  102. data/sig/kobako/sandbox.rbs +5 -24
  103. data/sig/kobako/sandbox_options.rbs +9 -2
  104. data/sig/kobako/snippet/binary.rbs +1 -1
  105. data/sig/kobako/snippet/source.rbs +1 -1
  106. data/sig/kobako/transport/call.rbs +21 -0
  107. data/sig/kobako/transport/dispatcher.rbs +9 -11
  108. data/sig/kobako/transport/error.rbs +11 -0
  109. data/sig/kobako/transport/reflection.rbs +15 -0
  110. data/sig/kobako/transport/run.rbs +1 -1
  111. data/sig/kobako/transport/yielder.rbs +2 -2
  112. data/sig/kobako/transport.rbs +8 -2
  113. data/sig/kobako/unresolved.rbs +4 -0
  114. metadata +30 -11
  115. data/lib/kobako/fault.rb +0 -38
  116. data/lib/kobako/outcome/panic.rb +0 -42
  117. data/lib/kobako/transport/request.rb +0 -76
  118. data/lib/kobako/transport/response.rb +0 -83
  119. data/lib/kobako/transport/yield.rb +0 -94
  120. data/sig/kobako/fault.rbs +0 -19
  121. data/sig/kobako/outcome/panic.rbs +0 -34
  122. data/sig/kobako/transport/request.rbs +0 -29
  123. data/sig/kobako/transport/response.rbs +0 -33
  124. data/sig/kobako/transport/yield.rbs +0 -34
@@ -0,0 +1,240 @@
1
+ //! The Reply envelope and its reverse-direction sibling, the Yield Reply.
2
+ //!
3
+ //! Success-versus-fault is decided by a tag byte rather than a reserved
4
+ //! payload value, so a side learns the outcome of a Call whatever schema
5
+ //! the payload carries.
6
+
7
+ use super::bytes::{Reader, Writer};
8
+ use super::{DecodeError, ErrorRecord, Fault};
9
+
10
+ const TAG_OK: u8 = 0;
11
+ const TAG_FAULT: u8 = 1;
12
+
13
+ const YIELD_OK: u8 = 0x01;
14
+ const YIELD_BREAK: u8 = 0x02;
15
+ const YIELD_RESERVED: u8 = 0x03;
16
+ const YIELD_ERROR: u8 = 0x04;
17
+
18
+ /// The answer to one dispatch Call.
19
+ #[derive(Debug, Clone, PartialEq, Eq)]
20
+ pub enum Reply {
21
+ /// The method returned; the bytes are the value, codec-encoded.
22
+ Ok(Vec<u8>),
23
+ /// The method refused or failed. Typed rather than codec-encoded:
24
+ /// every field of a Fault is kobako's, so a guest reads a refusal
25
+ /// whatever schema it speaks.
26
+ Fault(Fault),
27
+ }
28
+
29
+ impl Reply {
30
+ pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
31
+ let mut reader = Reader::new(bytes);
32
+ let tag = reader.u8()?;
33
+ match tag {
34
+ TAG_OK => Ok(Reply::Ok(reader.remaining().to_vec())),
35
+ TAG_FAULT => {
36
+ let fault = Fault::read(&mut reader)?;
37
+ reader.finish()?;
38
+ Ok(Reply::Fault(fault))
39
+ }
40
+ _ => Err(DecodeError::new("Reply tag must be 0 (ok) or 1 (fault)")),
41
+ }
42
+ }
43
+
44
+ pub fn encode(&self) -> Vec<u8> {
45
+ let mut writer = Writer::new();
46
+ match self {
47
+ Reply::Ok(body) => {
48
+ writer.u8(TAG_OK).remainder(body);
49
+ }
50
+ Reply::Fault(fault) => {
51
+ writer.u8(TAG_FAULT);
52
+ fault.write(&mut writer);
53
+ }
54
+ }
55
+ writer.into_bytes()
56
+ }
57
+ }
58
+
59
+ /// The answer to one Yield Call. Carries a `Break` outcome the dispatch
60
+ /// Reply has no counterpart for, because a block can end its caller.
61
+ #[derive(Debug, Clone, PartialEq, Eq)]
62
+ pub enum YieldReply {
63
+ /// The block completed; the bytes are its value, codec-encoded.
64
+ Ok(Vec<u8>),
65
+ /// The block ran `break`; the bytes are the break value,
66
+ /// codec-encoded. It returns to the guest rather than to host code.
67
+ Break(Vec<u8>),
68
+ /// The block raised, or failed in a way the host re-raises at the
69
+ /// yield site.
70
+ Error(ErrorRecord),
71
+ }
72
+
73
+ impl YieldReply {
74
+ pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
75
+ let mut reader = Reader::new(bytes);
76
+ let tag = reader.u8()?;
77
+ match tag {
78
+ YIELD_OK => Ok(YieldReply::Ok(reader.remaining().to_vec())),
79
+ YIELD_BREAK => Ok(YieldReply::Break(reader.remaining().to_vec())),
80
+ YIELD_ERROR => {
81
+ let record = ErrorRecord::read(&mut reader)?;
82
+ reader.finish()?;
83
+ Ok(YieldReply::Error(record))
84
+ }
85
+ YIELD_RESERVED => Err(DecodeError::new("Yield Reply tag 0x03 is reserved")),
86
+ _ => Err(DecodeError::new("Yield Reply tag is not recognised")),
87
+ }
88
+ }
89
+
90
+ pub fn encode(&self) -> Vec<u8> {
91
+ let mut writer = Writer::new();
92
+ match self {
93
+ YieldReply::Ok(body) => {
94
+ writer.u8(YIELD_OK).remainder(body);
95
+ }
96
+ YieldReply::Break(body) => {
97
+ writer.u8(YIELD_BREAK).remainder(body);
98
+ }
99
+ YieldReply::Error(record) => {
100
+ writer.u8(YIELD_ERROR);
101
+ record.write(&mut writer);
102
+ }
103
+ }
104
+ writer.into_bytes()
105
+ }
106
+ }
107
+
108
+ #[cfg(test)]
109
+ mod tests {
110
+ use super::*;
111
+
112
+ #[test]
113
+ fn both_reply_arms_round_trip() {
114
+ for reply in [
115
+ Reply::Ok(vec![0x2a]),
116
+ Reply::Fault(Fault::new(
117
+ super::super::FaultKind::Undefined,
118
+ "no such method",
119
+ )),
120
+ ] {
121
+ let encoded = reply.encode();
122
+ assert_eq!(
123
+ Reply::decode(&encoded),
124
+ Ok(reply),
125
+ "a Reply must survive a host encode and decode on both arms"
126
+ );
127
+ }
128
+ }
129
+
130
+ #[test]
131
+ fn an_empty_ok_body_round_trips() {
132
+ let encoded = Reply::Ok(Vec::new()).encode();
133
+ assert_eq!(
134
+ Reply::decode(&encoded),
135
+ Ok(Reply::Ok(Vec::new())),
136
+ "a Reply whose payload is empty must decode as an empty value, not a truncation"
137
+ );
138
+ }
139
+
140
+ #[test]
141
+ fn an_unknown_reply_tag_is_refused() {
142
+ assert!(
143
+ Reply::decode(&[9, 0]).is_err(),
144
+ "a Reply tag that is neither ok nor fault must be rejected as a wire violation"
145
+ );
146
+ }
147
+
148
+ #[test]
149
+ fn a_zero_length_reply_is_refused() {
150
+ assert!(
151
+ Reply::decode(&[]).is_err(),
152
+ "a zero-length Reply must be rejected rather than read as a missing tag"
153
+ );
154
+ }
155
+
156
+ #[test]
157
+ fn every_live_yield_arm_round_trips() {
158
+ let arms = [
159
+ YieldReply::Ok(vec![0xa4]),
160
+ YieldReply::Break(vec![0xa4]),
161
+ YieldReply::Error(ErrorRecord {
162
+ name: "LocalJumpError".into(),
163
+ message: "no block".into(),
164
+ backtrace: vec!["(eval):1".into()],
165
+ }),
166
+ ];
167
+ for arm in arms {
168
+ let encoded = arm.encode();
169
+ assert_eq!(
170
+ YieldReply::decode(&encoded),
171
+ Ok(arm),
172
+ "every live Yield Reply arm must survive a host encode and decode"
173
+ );
174
+ }
175
+ }
176
+
177
+ #[test]
178
+ fn the_reserved_yield_tag_is_refused() {
179
+ assert!(
180
+ YieldReply::decode(&[0x03]).is_err(),
181
+ "Yield Reply tag 0x03 must be rejected by both peers so it stays reserved"
182
+ );
183
+ }
184
+
185
+ #[test]
186
+ fn an_unknown_yield_tag_is_refused() {
187
+ assert!(
188
+ YieldReply::decode(&[0x7f]).is_err(),
189
+ "a Yield Reply tag outside the live set must be rejected as a wire violation"
190
+ );
191
+ }
192
+
193
+ #[test]
194
+ fn golden_layout_pins_the_reply_tags() {
195
+ // The same body on both arms: what separates "the Service returned
196
+ // this" from "the Service failed with this" is the tag alone, which
197
+ // is what lets a side read the outcome without a payload codec.
198
+ assert_eq!(
199
+ Reply::Ok(vec![0x2a]).encode(),
200
+ vec![0, 0x2a],
201
+ "a successful Reply must encode as tag byte 0 followed by the body alone"
202
+ );
203
+ assert_eq!(
204
+ Reply::Fault(Fault::new(super::super::FaultKind::Runtime, "x")).encode(),
205
+ vec![1, 0, 0, 0, 0, 1, b'x'],
206
+ "a fault Reply must encode as tag byte 1 followed by the Fault's own fixed \
207
+ layout, so a guest reads a refusal without reaching a payload codec"
208
+ );
209
+ }
210
+
211
+ #[test]
212
+ fn golden_layout_pins_the_yield_tags() {
213
+ assert_eq!(
214
+ YieldReply::Ok(vec![0xc0]).encode(),
215
+ vec![0x01, 0xc0],
216
+ "a Yield Reply carrying the block's value must encode as tag byte 0x01"
217
+ );
218
+ assert_eq!(
219
+ YieldReply::Break(vec![0xc0]).encode(),
220
+ vec![0x02, 0xc0],
221
+ "a Yield Reply carrying a break value must encode as tag byte 0x02"
222
+ );
223
+ assert_eq!(
224
+ YieldReply::Error(ErrorRecord {
225
+ name: "E".into(),
226
+ message: "m".into(),
227
+ backtrace: Vec::new(),
228
+ })
229
+ .encode(),
230
+ vec![
231
+ 0x04, // tag: error
232
+ 0, 0, 0, 1, b'E', // name
233
+ 0, 0, 0, 1, b'm', // message
234
+ 0, 0, 0, 0, // backtrace count
235
+ ],
236
+ "a Yield Reply carrying a guest failure must encode as tag byte 0x04 followed \
237
+ by an Error Record rather than a codec-encoded body"
238
+ );
239
+ }
240
+ }
@@ -0,0 +1,85 @@
1
+ //! The Run envelope — the host→guest request one `#run` invocation carries.
2
+ //!
3
+ //! Run is Call's reverse-direction sibling — `entrypoint` routes it, the
4
+ //! payload feeds it. It carries no `method` because the entrypoint is
5
+ //! invoked through its own `#call`, and no `block_given` because `#run`
6
+ //! supplies no block. It rides the command buffer rather than a stdin
7
+ //! frame, so its length reaches the guest as an export argument.
8
+
9
+ use super::bytes::{Reader, Writer};
10
+ use super::DecodeError;
11
+
12
+ /// One `#run` invocation: which top-level constant, and its arguments.
13
+ #[derive(Debug, Clone, PartialEq, Eq)]
14
+ pub struct Run {
15
+ pub entrypoint: String,
16
+ pub payload: Vec<u8>,
17
+ }
18
+
19
+ impl Run {
20
+ pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
21
+ let mut reader = Reader::new(bytes);
22
+ let entrypoint = reader.text()?.to_owned();
23
+ Ok(Run {
24
+ entrypoint,
25
+ payload: reader.remaining().to_vec(),
26
+ })
27
+ }
28
+
29
+ pub fn encode(&self) -> Vec<u8> {
30
+ let mut writer = Writer::new();
31
+ writer
32
+ .bytes(self.entrypoint.as_bytes())
33
+ .remainder(&self.payload);
34
+ writer.into_bytes()
35
+ }
36
+ }
37
+
38
+ #[cfg(test)]
39
+ mod tests {
40
+ use super::*;
41
+
42
+ #[test]
43
+ fn a_run_round_trips() {
44
+ let run = Run {
45
+ entrypoint: "Entry".into(),
46
+ payload: vec![0x92, 0x90, 0x80],
47
+ };
48
+ let encoded = run.encode();
49
+ assert_eq!(
50
+ Run::decode(&encoded),
51
+ Ok(run),
52
+ "a Run must survive a host encode and decode unchanged"
53
+ );
54
+ }
55
+
56
+ #[test]
57
+ fn a_run_with_no_arguments_round_trips() {
58
+ let run = Run {
59
+ entrypoint: "Entry".into(),
60
+ payload: Vec::new(),
61
+ };
62
+ let encoded = run.encode();
63
+ assert_eq!(
64
+ Run::decode(&encoded),
65
+ Ok(run),
66
+ "a Run with an empty payload must decode as empty, not as a truncation"
67
+ );
68
+ }
69
+
70
+ #[test]
71
+ fn golden_layout_pins_the_run_field_order() {
72
+ let run = Run {
73
+ entrypoint: "E".into(),
74
+ payload: vec![0xc0],
75
+ };
76
+ assert_eq!(
77
+ run.encode(),
78
+ vec![
79
+ 0, 0, 0, 1, b'E', // entrypoint
80
+ 0xc0, // payload remainder
81
+ ],
82
+ "the Run byte layout must stay fixed for both peers to agree"
83
+ );
84
+ }
85
+ }
@@ -0,0 +1,73 @@
1
+ //! The core envelope — the fixed-layout outer frame of every host↔guest
2
+ //! message.
3
+ //!
4
+ //! Routing and outcome attribution live here; everything a resolved method
5
+ //! consumes rides through as an opaque `payload` this layer never reads. A
6
+ //! tier that only routes messages therefore needs no payload codec at all,
7
+ //! and the one an assembly does use is its own choice.
8
+ //!
9
+ //! A decoded envelope borrows the buffer it came from, so a payload reaches
10
+ //! its reader as a view rather than a copy. The two fields a reader keeps
11
+ //! past that buffer's life — a binding's path and a snippet's body, read
12
+ //! from a frame at boot and consumed later — are copied at decode.
13
+ //!
14
+ //! No type here is `#[non_exhaustive]`. Every arm is a tag byte the layout
15
+ //! document fixes, so the set grows only with an ABI version — and when it
16
+ //! does, a peer that has not been taught the new arm should fail to
17
+ //! compile rather than fall into a wildcard and answer a message it cannot
18
+ //! read.
19
+ //!
20
+ //! This layer has one implementation, so its `golden_layout_*` tests are
21
+ //! what hold it to the layout document. They spell each tag as the literal
22
+ //! byte that document fixes, never as the constant beside it: a golden
23
+ //! written from the constant compares the implementation to itself.
24
+ //!
25
+ //! [core envelope]: ../../../docs/wire/envelope.md
26
+
27
+ pub(crate) mod bytes;
28
+ pub mod call;
29
+ pub mod error_record;
30
+ pub mod fault;
31
+ pub mod invocation_frames;
32
+ pub mod outcome;
33
+ pub mod reply;
34
+ pub mod run;
35
+
36
+ pub use call::{Call, Target};
37
+ pub use error_record::ErrorRecord;
38
+ pub use fault::{Fault, FaultKind};
39
+ pub use invocation_frames::{Bindings, Snippet, Snippets};
40
+ pub use outcome::{Origin, Outcome, Panic};
41
+ pub use reply::{Reply, YieldReply};
42
+ pub use run::Run;
43
+
44
+ use std::fmt;
45
+
46
+ /// A message that does not conform to the core envelope layout. Only
47
+ /// decoding produces one — every `encode` is infallible — so the name says
48
+ /// which direction it comes from.
49
+ ///
50
+ /// The reason is a fixed string because every case is a wire violation the
51
+ /// receiving side rejects rather than a condition a caller recovers from by
52
+ /// inspecting it; it stays behind `message` so a later reason can carry more
53
+ /// than a literal.
54
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
55
+ pub struct DecodeError(&'static str);
56
+
57
+ impl DecodeError {
58
+ pub const fn new(reason: &'static str) -> Self {
59
+ DecodeError(reason)
60
+ }
61
+
62
+ pub fn message(&self) -> &str {
63
+ self.0
64
+ }
65
+ }
66
+
67
+ impl fmt::Display for DecodeError {
68
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69
+ f.write_str(self.0)
70
+ }
71
+ }
72
+
73
+ impl std::error::Error for DecodeError {}
@@ -0,0 +1,21 @@
1
+ //! kobako-transport — the fixed tier of the kobako wire.
2
+ //!
3
+ //! A host, a payload codec, and a guest are chosen independently only
4
+ //! because two things are the same in every assembly, and this crate is
5
+ //! both of them:
6
+ //!
7
+ //! - `envelope` — the outer frame every host↔guest message rides in,
8
+ //! carrying routing and outcome attribution and handing the payload
9
+ //! through as bytes it never reads
10
+ //! - `abi` — the version, the packed-return layout, the frame prefix, and
11
+ //! the size cap the two sides must already agree on to exchange a byte
12
+ //!
13
+ //! Every value here is spelled once. Nothing depends on an engine, an
14
+ //! interpreter, or a schema — and nothing may come to: a tier every other
15
+ //! tier composes against cannot carry a choice any of them might want to
16
+ //! make differently.
17
+ //!
18
+ //! [core envelope]: ../../../docs/wire/envelope.md
19
+
20
+ pub mod abi;
21
+ pub mod envelope;
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.12.0...kobako-wasmtime-v0.13.0) (2026-07-29)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **wasmtime:** put every cap in one struct, named the way the field is read
9
+ * **runtime:** give the engine contract names an implementer can write
10
+ * **transport:** name the core envelope's types after what they carry
11
+ * **wire:** route every tier through the one envelope
12
+ * **wire:** carry dispatch over the core envelope with an opaque payload
13
+
14
+ ### Features
15
+
16
+ * **wire:** carry dispatch over the core envelope with an opaque payload ([556104b](https://github.com/elct9620/kobako/commit/556104bf86fdf481b5368b70af83eb0add4b2708))
17
+
18
+
19
+ ### Code Refactoring
20
+
21
+ * **runtime:** give the engine contract names an implementer can write ([0a41491](https://github.com/elct9620/kobako/commit/0a41491360cf17def8245c0758679e92572b990f))
22
+ * **transport:** name the core envelope's types after what they carry ([b44de65](https://github.com/elct9620/kobako/commit/b44de6502953826c567d4ef8a594561479f50c1d))
23
+ * **wasmtime:** put every cap in one struct, named the way the field is read ([3f57860](https://github.com/elct9620/kobako/commit/3f578608b044d1ade0ae35231266cf3c9e517c02))
24
+ * **wire:** route every tier through the one envelope ([c5cd33a](https://github.com/elct9620/kobako/commit/c5cd33a5346f49857fa6e1f45c9cf9b9bea0ff77))
25
+
26
+ ## [0.12.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.11.0...kobako-wasmtime-v0.12.0) (2026-07-24)
27
+
28
+
29
+ ### Miscellaneous Chores
30
+
31
+ * **kobako-wasmtime:** Synchronize kobako crates versions
32
+
3
33
  ## [0.11.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.10.2...kobako-wasmtime-v0.11.0) (2026-07-19)
4
34
 
5
35
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  [package]
17
17
  name = "kobako-wasmtime"
18
- version = "0.11.0"
18
+ version = "0.13.0"
19
19
  edition = "2021"
20
20
  description = "wasmtime implementation of the kobako host runtime contract."
21
21
  license = "Apache-2.0"
@@ -28,7 +28,10 @@ categories = ["wasm", "virtualization"]
28
28
  # The engine-neutral contract this crate implements. The version pin
29
29
  # rides the linked release group; the path keeps in-tree builds (and
30
30
  # the Ruby gem, which ships both crates) resolving locally.
31
- kobako-runtime = { version = "0.11.0", path = "../kobako-runtime" }
31
+ kobako-runtime = { version = "0.13.0", path = "../kobako-runtime" }
32
+ # The fixed tier: the core envelope this driver shuttles and the ABI
33
+ # values it drives the exports against.
34
+ kobako-transport = { version = "0.13.0", path = "../kobako-transport" }
32
35
  # wasmtime — host-side embedder for kobako.wasm. We disable default-features
33
36
  # and opt back in only what kobako needs: a Cranelift-backed runtime that can
34
37
  # compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
@@ -24,7 +24,7 @@ SDK consumes the same surface.
24
24
 
25
25
  ```toml
26
26
  [dependencies]
27
- kobako-wasmtime = "0.11.0" # x-release-please-version
27
+ kobako-wasmtime = "0.13.0" # x-release-please-version
28
28
  ```
29
29
 
30
30
  ## License
@@ -1,29 +1,36 @@
1
1
  //! Per-`Driver` execution configuration.
2
2
  //!
3
- //! The wall-clock and per-channel capture caps a frontend forwards into
4
- //! `Driver::new`. A plain value carrier owned by the `Driver` — distinct
5
- //! from the process-wide engine/module `crate::cache` (which is shared
6
- //! across every sandbox) and from the per-invocation
7
- //! `crate::invocation::Invocation` (which the wasm engine mutates from
8
- //! inside a run). These caps are read only by `Driver` methods between
9
- //! runs, so they live here.
3
+ //! Every cap a frontend forwards into `Driver::new`. A plain value carrier
4
+ //! owned by the `Driver` — distinct from the process-wide engine/module
5
+ //! `crate::cache` (which is shared across every sandbox) and from the
6
+ //! per-invocation `crate::invocation::Invocation` (which the wasm engine
7
+ //! mutates from inside a run).
8
+ //!
9
+ //! Field-for-field the same shape as the SDK's `kobako::Options`, and
10
+ //! deliberately not shared with it: this struct is free to name a wasmtime
11
+ //! type, and `Options` is not — an engine-free host must still be able to
12
+ //! build the SDK.
10
13
 
11
14
  use std::time::Duration;
12
15
 
13
16
  use kobako_runtime::profile::Profile;
14
17
 
15
- /// Wall-clock and output caps plus the requested isolation profile for
16
- /// one `Driver`. `None` on any cap field disables that cap.
18
+ /// The caps and requested isolation profile for one `Driver`. `None` on
19
+ /// any cap field disables that cap.
17
20
  pub struct Config {
18
21
  /// Wall-clock cap for one guest `#eval` / `#run`. Stamped into a
19
22
  /// per-run `Instant` deadline by `Driver::prime_caps`.
20
23
  pub timeout: Option<Duration>,
24
+ /// Guest linear-memory cap, in bytes. Threaded into each fresh
25
+ /// `Invocation`, where the wasmtime `ResourceLimiter` callback
26
+ /// consumes it from inside the engine.
27
+ pub memory_limit: Option<usize>,
21
28
  /// Byte cap for guest stdout capture.
22
29
  /// Sizes the per-run `MemoryOutputPipe` and computes the truncation
23
30
  /// flag in `Driver::build_snapshot`.
24
- pub stdout_limit_bytes: Option<usize>,
25
- /// Byte cap for guest stderr capture. Mirror of `stdout_limit_bytes`.
26
- pub stderr_limit_bytes: Option<usize>,
31
+ pub stdout_limit: Option<usize>,
32
+ /// Byte cap for guest stderr capture. Mirror of `stdout_limit`.
33
+ pub stderr_limit: Option<usize>,
27
34
  /// Isolation posture the frontend requested. The per-invocation
28
35
  /// WASI context is built to this rung — `Hermetic` freezes ambient
29
36
  /// time and entropy (`crate::ambient`), `Permissive` leaves the
@@ -5,19 +5,19 @@
5
5
  //! through the closure registered by `instance_pre::build_linker`.
6
6
  //! That closure delegates here. The dispatcher:
7
7
  //!
8
- //! 1. Reads the Request bytes from guest linear memory.
8
+ //! 1. Reads the Call bytes from guest linear memory.
9
9
  //! 2. Invokes the bound `DispatchHandler` (the frontend's dispatch
10
- //! bridge, e.g. a Ruby Proc) and recovers Response bytes.
10
+ //! bridge, e.g. a Ruby Proc) and recovers Reply bytes.
11
11
  //! 3. Allocates a guest buffer via `__kobako_alloc(len)` invoked
12
12
  //! through `Caller::get_export`.
13
- //! 4. Writes the Response bytes into the guest buffer.
13
+ //! 4. Writes the Reply bytes into the guest buffer.
14
14
  //! 5. Returns packed `(ptr<<32)|len` for the guest to decode.
15
15
  //!
16
16
  //! Returns 0 on any step failure. `Kobako::Sandbox#initialize` always
17
17
  //! installs the dispatch handler before any invocation, so reaching the
18
18
  //! dispatcher with no handler bound is itself a wire-layer fault; the
19
19
  //! guest maps a 0 return to a trap. Failures during normal dispatch
20
- //! surface as Response.err envelopes from
20
+ //! take the Reply's fault arm from
21
21
  //! `Kobako::Transport::Dispatcher.dispatch` itself — they never reach
22
22
  //! this 0-return path.
23
23
  //!
@@ -47,6 +47,8 @@
47
47
 
48
48
  use wasmtime::Caller;
49
49
 
50
+ use kobako_transport::envelope::Call;
51
+
50
52
  use crate::invocation::Invocation;
51
53
 
52
54
  /// Drive a single `__kobako_dispatch` invocation end-to-end. Entry point
@@ -56,7 +58,7 @@ use crate::invocation::Invocation;
56
58
  /// wire-layer fault. Failure paths log a `[kobako-dispatch]` line to
57
59
  /// `stderr` so operators have a breadcrumb when the guest sees a 0
58
60
  /// return and traps. The bound dispatch handler is contracted never to
59
- /// raise (it folds Service exceptions into Response.err envelopes),
61
+ /// raise (it folds Service exceptions onto the Reply's fault arm),
60
62
  /// so reaching the failure path is always a wiring bug or wire-layer
61
63
  /// fault rather than an expected path.
62
64
  pub(crate) fn handle(caller: &mut Caller<'_, Invocation>, req_ptr: i32, req_len: i32) -> i64 {
@@ -77,6 +79,11 @@ fn try_handle(
77
79
  req_len: i32,
78
80
  ) -> Result<i64, &'static str> {
79
81
  let req_bytes = crate::guest_mem::read(caller, req_ptr, req_len)?;
82
+ // The driver decodes the core envelope so the frontend never sees a
83
+ // frame; the payload inside it stays bytes the whole way through.
84
+ let call = Call::decode(&req_bytes).map_err(|_| {
85
+ "the guest sent a malformed Call envelope — please report this as a kobako bug"
86
+ })?;
80
87
 
81
88
  // `Kobako::Sandbox` always installs the dispatch handler before
82
89
  // invoking the runtime, so reaching this branch indicates a misuse
@@ -90,15 +97,15 @@ fn try_handle(
90
97
  // handler. The borrow ends with the block, freeing the Caller for
91
98
  // `write_response`; nested dispatch frames each build their own, so
92
99
  // the LIFO re-entry lives on the Rust stack — no shared slot.
93
- let resp_bytes = {
100
+ let reply = {
94
101
  let mut yielder = crate::guest_mem::CallerYielder::new(caller);
95
- handler.dispatch(&req_bytes, &mut yielder)
102
+ handler.dispatch(call, &mut yielder)
96
103
  }
97
104
  .ok_or(
98
105
  "a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
99
106
  )?;
100
107
 
101
- write_response(caller, &resp_bytes)
108
+ write_response(caller, &reply.encode())
102
109
  }
103
110
 
104
111
  /// Allocate a guest-side buffer and copy the response bytes into it via