kobako 0.12.1-x86_64-linux → 0.12.2-x86_64-linux

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +11 -0
  4. data/README.md +1 -1
  5. data/crates/kobako-runtime/CHANGELOG.md +8 -0
  6. data/crates/kobako-runtime/README.md +34 -0
  7. data/crates/kobako-wasmtime/CHANGELOG.md +8 -0
  8. data/crates/kobako-wasmtime/README.md +32 -0
  9. data/data/kobako.wasm +0 -0
  10. data/lib/kobako/3.3/kobako.so +0 -0
  11. data/lib/kobako/3.4/kobako.so +0 -0
  12. data/lib/kobako/4.0/kobako.so +0 -0
  13. data/lib/kobako/catalog/handles.rb +3 -3
  14. data/lib/kobako/catalog/namespaces.rb +4 -0
  15. data/lib/kobako/catalog/snippets.rb +4 -0
  16. data/lib/kobako/codec/encoder.rb +5 -1
  17. data/lib/kobako/codec/factory.rb +41 -13
  18. data/lib/kobako/codec/handle_walk.rb +4 -0
  19. data/lib/kobako/errors.rb +18 -16
  20. data/lib/kobako/sandbox.rb +20 -18
  21. data/lib/kobako/sandbox_options.rb +25 -9
  22. data/lib/kobako/snapshot.rb +7 -13
  23. data/lib/kobako/transport/dispatcher.rb +2 -2
  24. data/lib/kobako/transport/response.rb +14 -14
  25. data/lib/kobako/transport/run.rb +2 -6
  26. data/lib/kobako/transport/yield.rb +1 -1
  27. data/lib/kobako/transport/yielder.rb +2 -2
  28. data/lib/kobako/version.rb +1 -1
  29. data/release-please-config.json +48 -3
  30. data/sig/kobako/codec/factory.rbs +3 -0
  31. data/sig/kobako/errors.rbs +7 -14
  32. data/sig/kobako/runtime.rbs +8 -3
  33. data/sig/kobako/sandbox.rbs +2 -2
  34. data/sig/kobako/sandbox_options.rbs +4 -2
  35. data/sig/kobako/snapshot.rbs +0 -3
  36. data/sig/kobako/transport/dispatcher.rbs +1 -1
  37. data/sig/kobako/transport/run.rbs +2 -2
  38. data/sig/kobako/transport/yielder.rbs +2 -2
  39. data/sig/kobako/transport.rbs +8 -0
  40. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cbd9774c84737000b2be506f6a0334702ed103889df9ac26008f174973cb048e
4
- data.tar.gz: 48dcbc87ab4227394ffd0efe751b338ac8b957c5a4979c56e0ee8f6fdc98e033
3
+ metadata.gz: 425bd29857c91850c1d3920343c44f11dbc425988cd4fb6c400ba638b8b02b1f
4
+ data.tar.gz: 1472552c0062fa1af7879f5e07a9ae54eaa74419ca7c66c6a253607b95388625
5
5
  SHA512:
6
- metadata.gz: f7eb01555a4d46ba57359a643e8c43a493f6ae0a8f05119df3f3c2ab2b679bb6f1f3a07f06d27735a0c77e2b0d0b22ffb41464b3b63d4ed71ce355666d7e7e0a
7
- data.tar.gz: 7e21e70dd618e3c3996b68ee68e463b8f5902ee19ed3e6e93eff4ea48f8084779e2c27e72c7f057ea57856e00ad596796a803d92e2e15a6959b3177c69cd682a
6
+ metadata.gz: 7f6faee72e9e29f491d08a8b2d08430127fc1da78aac834a09dccd833ab97f13cb26cd7360e9b6ce9ec323599152a57d8bdb38d744a968e0eadb7a7a3b0296d9
7
+ data.tar.gz: c425b0a041be572adf2f66faae10a66a38efce1142399d343d9c86e09308537a577a76f3a5399a3282319c3559970e8a9bb9a638c26cc6633a69998b2bf05630
@@ -1 +1 @@
1
- {".":"0.12.1","wasm/kobako-core":"0.6.0","wasm/kobako":"0.6.0","wasm/kobako-io":"0.6.0","wasm/kobako-json":"0.6.0","wasm/kobako-regexp":"0.6.0","wasm/kobako-baker":"0.6.0"}
1
+ {".":"0.12.2","wasm/kobako-core":"0.6.1","wasm/kobako":"0.6.1","wasm/kobako-io":"0.6.1","wasm/kobako-json":"0.6.1","wasm/kobako-regexp":"0.6.1","wasm/kobako-baker":"0.6.1","crates/kobako-runtime":"0.6.1","crates/kobako-wasmtime":"0.6.1"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.2](https://github.com/elct9620/kobako/compare/v0.12.1...v0.12.2) (2026-07-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **codec:** bound ext-envelope nesting to keep deep Fault chains off the native stack ([7bed2b2](https://github.com/elct9620/kobako/commit/7bed2b2f43a63538aa60610c82d2eb65bcce7b15))
9
+ * **guest:** size collection conversions by C array length, not #length ([90ecbd0](https://github.com/elct9620/kobako/commit/90ecbd0cb6a990b8c5a1e5deec3a10df4eaa37df))
10
+ * **io:** enforce the fd allowlist at the write syscall ([1b300df](https://github.com/elct9620/kobako/commit/1b300df7bee8f87b701f76b42300163a8899b93e))
11
+ * **release:** advance last-release-sha past the unparseable fork merge ([d06117d](https://github.com/elct9620/kobako/commit/d06117d462eea0ae5648e5bdd6886b735765f3b3))
12
+ * **sandbox:** honor nil to disable the output caps, and validate them ([51d1e90](https://github.com/elct9620/kobako/commit/51d1e900e3d7d659ffd432ff3d613786e9073b05))
13
+
3
14
  ## [0.12.1](https://github.com/elct9620/kobako/compare/v0.12.0...v0.12.1) (2026-06-27)
4
15
 
5
16
 
data/README.md CHANGED
@@ -127,7 +127,7 @@ end
127
127
  |---------------------------------|----------------|------------------------------------------------------|
128
128
  | `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
129
129
  | `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
130
- | `Kobako::HandlerExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
130
+ | `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
131
131
  | `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
132
132
 
133
133
  `SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` / `details` fields when the guest produced a panic envelope.
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.6.0...kobako-runtime-v0.6.1) (2026-07-02)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **kobako-runtime:** Synchronize kobako crates versions
@@ -0,0 +1,34 @@
1
+ # kobako-runtime
2
+
3
+ Engine-neutral host runtime contract for
4
+ [kobako](https://github.com/elct9620/kobako), an in-process Wasm
5
+ sandbox for running untrusted mruby scripts.
6
+
7
+ A kobako host drives a Guest Binary through a wasm engine; this crate
8
+ is the surface where the two meet, free of any engine or frontend
9
+ type, so the engine stays swappable:
10
+
11
+ - `runtime` — the `Runtime` trait: one guest invocation on a fresh
12
+ instance in, its observable `Snapshot` out
13
+ - `snapshot` — the per-invocation observables: `Completion` (outcome
14
+ or trap), the two output `Capture`s, and resource `Usage`, uniform
15
+ across success and trap
16
+ - `error` — the neutral failure channels: `Trap` (engine fault) and
17
+ `SetupError` (the invocation never started)
18
+ - `dispatch` / `yielder` — the `DispatchHandler` and `Yielder` traits
19
+ a frontend supplies for guest→host dispatch and block-yield re-entry
20
+
21
+ Engine implementations (such as `kobako-wasmtime`) implement
22
+ `Runtime`; host frontends (such as the kobako Ruby gem's native ext)
23
+ map the neutral types onto their own language surface.
24
+
25
+ ## Usage
26
+
27
+ ```toml
28
+ [dependencies]
29
+ kobako-runtime = "0.6.1" # x-release-please-version
30
+ ```
31
+
32
+ ## License
33
+
34
+ Apache-2.0
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.6.1](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.6.0...kobako-wasmtime-v0.6.1) (2026-07-02)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * **kobako-wasmtime:** Synchronize kobako crates versions
@@ -0,0 +1,32 @@
1
+ # kobako-wasmtime
2
+
3
+ The [wasmtime](https://wasmtime.dev) implementation of the
4
+ [kobako](https://github.com/elct9620/kobako) host runtime contract
5
+ ([`kobako-runtime`](https://crates.io/crates/kobako-runtime)).
6
+
7
+ `Driver` implements the contract's `Runtime` trait over wasmtime and
8
+ owns every engine-bound mechanic, so frontends see only the neutral
9
+ contract surface:
10
+
11
+ - process-wide Engine and compiled-Module caches with an on-disk AOT
12
+ (`.cwasm`) artifact cache keyed by Guest Binary content
13
+ - a pre-linked `InstancePre` per guest path; every invocation runs on
14
+ a fresh instance and discards its Store afterwards
15
+ - the epoch-based wall-clock timeout and the per-invocation
16
+ linear-memory cap
17
+ - ambient denial: frozen WASI clocks and a constant RNG, so a guest
18
+ observes no real time and no real entropy
19
+
20
+ The kobako Ruby gem's native ext is the first frontend; a Rust host
21
+ SDK consumes the same surface.
22
+
23
+ ## Usage
24
+
25
+ ```toml
26
+ [dependencies]
27
+ kobako-wasmtime = "0.6.1" # x-release-please-version
28
+ ```
29
+
30
+ ## License
31
+
32
+ Apache-2.0
data/data/kobako.wasm CHANGED
Binary file
Binary file
Binary file
Binary file
@@ -38,7 +38,7 @@ module Kobako
38
38
  # for it. +object+ is any host-side Ruby object to bind. Returns a
39
39
  # freshly-allocated +Kobako::Handle+ whose +#id+ falls in
40
40
  # +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
41
- # +Kobako::HandlerExhaustedError+ if the next ID would exceed the
41
+ # +Kobako::HandleExhaustedError+ if the next ID would exceed the
42
42
  # cap. The cap is anchored on +Kobako::Handle+ — the wire codec
43
43
  # and the allocator share the same invariant.
44
44
  #
@@ -96,12 +96,12 @@ module Kobako
96
96
  end
97
97
 
98
98
  # Guard {#alloc} against issuing an ID past the cap. Returns +nil+
99
- # on success; raises +Kobako::HandlerExhaustedError+ at exhaustion.
99
+ # on success; raises +Kobako::HandleExhaustedError+ at exhaustion.
100
100
  def ensure_capacity!
101
101
  cap = Kobako::Handle::MAX_ID
102
102
  return unless @next_id > cap
103
103
 
104
- raise HandlerExhaustedError,
104
+ raise HandleExhaustedError,
105
105
  "Out of handle allocations: too many host objects were referenced " \
106
106
  "in a single invocation (limit #{cap})"
107
107
  end
@@ -82,6 +82,10 @@ module Kobako
82
82
  # first invocation, so the preamble is exactly the bindings that
83
83
  # existed at that moment — a bind reaching a +Kobako::Namespace+
84
84
  # after the seal raises +ArgumentError+ and never alters Frame 1.
85
+ # The memo is gated on the seal rather than dropped per mutation (the
86
+ # +Catalog::Snippets#encode+ approach) because a +Member+ bind lands
87
+ # on a child +Kobako::Namespace+, invisible to this collection; only
88
+ # the seal guarantees nothing further can change.
85
89
  def encode
86
90
  return @encoded if @encoded
87
91
 
@@ -44,6 +44,10 @@ module Kobako
44
44
  # The bytes are memoized — the table is replayed verbatim on every
45
45
  # invocation after sealing, so Frame 3 never changes between
46
46
  # encodes; {#register} drops the memo while the table is still open.
47
+ # Unlike +Catalog::Namespaces#encode+, which gates its memo on the
48
+ # seal, this one can fill eagerly and invalidate in +#register+
49
+ # because every mutation funnels through that single method — there is
50
+ # no out-of-sight child object to change the result behind its back.
47
51
  def encode
48
52
  return @encoded if @encoded
49
53
 
@@ -26,7 +26,11 @@ module Kobako
26
26
  # mapping is a closed set, and anything outside it is rejected by
27
27
  # the msgpack gem itself (arbitrary objects raise +NoMethodError+
28
28
  # from missing +to_msgpack+, integers outside i64..u64 raise
29
- # +RangeError+).
29
+ # +RangeError+). The +NoMethodError+ catch is deliberately broad:
30
+ # MessagePack signals "no wire representation" only through that error,
31
+ # so there is no narrower discriminator — a packer-internal
32
+ # +NoMethodError+ is likewise reported as +UnsupportedType+ rather than
33
+ # propagating.
30
34
  def self.encode(value)
31
35
  Factory.dump(value)
32
36
  rescue ::RangeError, ::NoMethodError => e
@@ -48,6 +48,16 @@ module Kobako
48
48
  EXT_ERRENV = 0x02
49
49
  private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
50
50
 
51
+ # An ext 0x02 (Fault) envelope nests through its +details+ field, and
52
+ # each level re-enters the codec with a fresh +MessagePack+ unpacker
53
+ # whose built-in stack guard resets — so ext-envelope depth is tracked
54
+ # here instead. The cap matches the wire's overall nesting bound and
55
+ # keeps a nested chain from exhausting the native stack: an over-deep
56
+ # chain fails as a clean wire error, never a stack-level trap.
57
+ MAX_EXT_DEPTH = 128
58
+ EXT_DEPTH_KEY = :__kobako_codec_ext_depth__
59
+ private_constant :MAX_EXT_DEPTH, :EXT_DEPTH_KEY
60
+
51
61
  # Instance-level pass-through onto the wrapped +MessagePack::Factory+.
52
62
  # Spelled +def_instance_delegators+ rather than +def_delegators+ because
53
63
  # the class also extends +SingleForwardable+ (see the +extend+ block
@@ -129,9 +139,13 @@ module Kobako
129
139
  # Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
130
140
  # the embedded payload flows through the same boundary as a top-level
131
141
  # encode — nested kobako values (Handle, nested Fault) reach the
132
- # registered ext-type packers via the cached singleton.
142
+ # registered ext-type packers via the cached singleton. A +details+
143
+ # chain nested past {MAX_EXT_DEPTH} has no wire representation and
144
+ # surfaces as +UnsupportedType+.
133
145
  def pack_fault(fault)
134
- Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
146
+ within_ext_frame(UnsupportedType) do
147
+ Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
148
+ end
135
149
  end
136
150
 
137
151
  # Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
@@ -139,19 +153,33 @@ module Kobako
139
153
  # +ArgumentError+ invariants surface as +InvalidType+ through the
140
154
  # decoder boundary. Inner decode goes through {Decoder} (not
141
155
  # +factory.load+) so the embedded +str+ payloads flow through the
142
- # same UTF-8 validation as a top-level decode.
143
- #
144
- # This establishes a runtime cycle Factory Decoder Factory: the
145
- # singleton instance feeds +Decoder.decode+, which re-enters this
146
- # method when a nested ext 0x02 appears inside +details+. The recursion
147
- # is bounded by msgpack nesting depth — identical to nested Array /
148
- # Hash payloads — so no extra guard is needed. Do not switch back to
149
- # +factory.load+ to "simplify": that path bypasses UTF-8 validation.
156
+ # same UTF-8 validation as a top-level decode. A nested ext 0x02 in
157
+ # +details+ re-enters this method, so {#within_ext_frame} bounds the
158
+ # chain depth to keep it from exhausting the native stack.
150
159
  def unpack_fault(payload)
151
- Decoder.decode(payload) do |map|
152
- raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
160
+ within_ext_frame(InvalidType) do
161
+ Decoder.decode(payload) do |map|
162
+ raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
163
+
164
+ Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
165
+ end
166
+ end
167
+ end
153
168
 
154
- Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
169
+ # Track ext-envelope re-entry depth and refuse a chain past
170
+ # {MAX_EXT_DEPTH}, raising +over_limit+ so the failure lands in the
171
+ # caller's existing wire-error class. The counter is thread-scoped and
172
+ # balanced by the +ensure+, so a raise mid-chain still unwinds it to
173
+ # its entry value.
174
+ def within_ext_frame(over_limit)
175
+ depth = (Thread.current[EXT_DEPTH_KEY] || 0) + 1
176
+ raise over_limit, "ext envelope nesting exceeds #{MAX_EXT_DEPTH} levels" if depth > MAX_EXT_DEPTH
177
+
178
+ Thread.current[EXT_DEPTH_KEY] = depth
179
+ begin
180
+ yield
181
+ ensure
182
+ Thread.current[EXT_DEPTH_KEY] = depth - 1
155
183
  end
156
184
  end
157
185
  end
@@ -97,6 +97,10 @@ module Kobako
97
97
  case value
98
98
  when ::Array then value.map { |element| HandleWalk.deep_restore(element, handler) }
99
99
  when ::Hash
100
+ # Rebuilt with each key restored: two distinct Handle keys that
101
+ # resolve to equal host objects collapse to the later pair, as in
102
+ # any Ruby Hash. The guest authored this payload, so that collapse
103
+ # is its own concern, not a fidelity guarantee the host owes it.
100
104
  value.to_h { |key, val| [HandleWalk.deep_restore(key, handler), HandleWalk.deep_restore(val, handler)] }
101
105
  when Kobako::Handle then handler.fetch(value.id)
102
106
  else value
data/lib/kobako/errors.rb CHANGED
@@ -35,7 +35,7 @@ module Kobako
35
35
  #
36
36
  # * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
37
37
  # at +wasm_path+.
38
- # * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit.
38
+ # * {HandleExhaustedError} < {SandboxError} — Handle id cap hit.
39
39
 
40
40
  # Base for all kobako-raised errors so callers that want to ignore the
41
41
  # taxonomy can rescue a single class.
@@ -89,10 +89,13 @@ module Kobako
89
89
  # +ModuleNotBuiltError+ first.
90
90
  class ModuleNotBuiltError < SetupError; end
91
91
 
92
- # Sandbox / wire layer. Raised when the guest ran to completion but
93
- # execution failed due to a mruby script error, a protocol fault, or a
94
- # host-side wire decode failure on an otherwise valid outcome tag.
95
- class SandboxError < Error
92
+ # The structured attribution the two invocation-failure classes carry
93
+ # from a decoded guest exception its +origin+, original +klass+,
94
+ # +backtrace_lines+, and +details+ so a Host App can inspect a failure
95
+ # beyond its message. Mixed into both rather than promoted to a shared
96
+ # superclass because +SandboxError+ and +ServiceError+ sit in distinct
97
+ # branches of the invocation-outcome taxonomy under +Kobako::Error+.
98
+ module StructuredError
96
99
  attr_reader :origin, :klass, :backtrace_lines, :details
97
100
 
98
101
  def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
@@ -104,26 +107,25 @@ module Kobako
104
107
  end
105
108
  end
106
109
 
110
+ # Sandbox / wire layer. Raised when the guest ran to completion but
111
+ # execution failed due to a mruby script error, a protocol fault, or a
112
+ # host-side wire decode failure on an otherwise valid outcome tag.
113
+ class SandboxError < Error
114
+ include StructuredError
115
+ end
116
+
107
117
  # Service layer. Raised when a Service capability call inside a mruby
108
118
  # script reported an application-level failure that the script did not
109
119
  # rescue.
110
120
  class ServiceError < Error
111
- attr_reader :origin, :klass, :backtrace_lines, :details
112
-
113
- def initialize(message, origin: nil, klass: nil, backtrace_lines: nil, details: nil)
114
- super(message)
115
- @origin = origin
116
- @klass = klass
117
- @backtrace_lines = backtrace_lines
118
- @details = details
119
- end
121
+ include StructuredError
120
122
  end
121
123
 
122
- # HandlerExhaustedError is the canonical SandboxError subclass for the
124
+ # HandleExhaustedError is the canonical SandboxError subclass for the
123
125
  # id-cap-hit path. Raised when the per-invocation Handle ID counter in
124
126
  # Catalog::Handles reaches +0x7fff_ffff+ (2³¹ − 1) and further
125
127
  # allocation would exceed the cap.
126
- class HandlerExhaustedError < SandboxError; end
128
+ class HandleExhaustedError < SandboxError; end
127
129
 
128
130
  # BytecodeError is the SandboxError subclass raised when a
129
131
  # `#preload(binary:)` snippet fails structural validation during the
@@ -89,7 +89,9 @@ module Kobako
89
89
  # normalisation. The constructed +SandboxOptions+ is exposed as
90
90
  # +#options+ and the four caps remain readable directly on Sandbox via
91
91
  # +Forwardable+ delegation.
92
- def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
92
+ def initialize(wasm_path: nil,
93
+ stdout_limit: SandboxOptions::DEFAULT_OUTPUT_LIMIT,
94
+ stderr_limit: SandboxOptions::DEFAULT_OUTPUT_LIMIT,
93
95
  timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
94
96
  memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
95
97
  @wasm_path = wasm_path || Kobako::Runtime.default_path
@@ -207,17 +209,17 @@ module Kobako
207
209
 
208
210
  private
209
211
 
210
- # Configure the +Runtime+'s host↔guest dispatch wiring. Builds a
211
- # lambda that re-enters the guest via
212
- # +Runtime#yield_to_active_invocation+ and a dispatch +Proc+ that routes
213
- # guest→host calls through the stateless +Transport::Dispatcher+,
214
- # capturing +@services+ / +@handler+ in the closure. Both are registered
215
- # on the +Runtime+ once at construction time so the wasm ext callback can
216
- # fire without further setup.
212
+ # Configure the +Runtime+'s host↔guest dispatch wiring. Registers a
213
+ # dispatch +Proc+ that routes guest→host calls through the stateless
214
+ # +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
215
+ # closure. The ext hands the +Proc+ a per-dispatch +guest_yielder+ — a
216
+ # +String String+ callable that re-enters the in-flight guest to run a
217
+ # yielded block — which the +Dispatcher+ forwards to the +Transport::Yielder+
218
+ # it builds for the call. Registered once at construction time so the
219
+ # wasm ext callback can fire without further setup.
217
220
  def install_dispatch_proc!
218
- yield_to_guest = ->(args_bytes) { @runtime.yield_to_active_invocation(args_bytes) }
219
- @runtime.on_dispatch = lambda do |request_bytes|
220
- Transport::Dispatcher.dispatch(request_bytes, @services, @handler, yield_to_guest)
221
+ @runtime.on_dispatch = lambda do |request_bytes, guest_yielder|
222
+ Transport::Dispatcher.dispatch(request_bytes, @services, @handler, guest_yielder)
221
223
  end
222
224
  end
223
225
 
@@ -226,8 +228,7 @@ module Kobako
226
228
  # state — capture buffers, truncation predicates, and the
227
229
  # +Catalog::Handles+ counter — before the guest runs. The
228
230
  # +Catalog::Handles+ itself is held as +@handler+ and never exposed
229
- # beyond this class: SPEC.md Terminology pins it as "Not exposed to the
230
- # Host App".
231
+ # beyond this class it is not part of the Host App's surface.
231
232
  def begin_invocation!
232
233
  @services.seal!
233
234
  @handler.reset!
@@ -239,9 +240,10 @@ module Kobako
239
240
  # the +invoke!+ +ensure+ block so the usage record is populated on
240
241
  # every outcome — value return, +Kobako::TrapError+ (including
241
242
  # +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
242
- # and +Kobako::ServiceError+. On the success path the same figures
243
- # already arrive via +Snapshot#usage+; on the trap path the Snapshot
244
- # never reaches Ruby so the ext readout here is the only source.
243
+ # and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
244
+ # both paths: the figures are stashed in the ext on every outcome, so
245
+ # unlike the +Snapshot+ (built only on success) the readout here also
246
+ # covers the trap path.
245
247
  #
246
248
  # The ext returns a positional 2-tuple +[wall_time, memory_peak]+
247
249
  # whose order matches the +Kobako::Usage+ field order; the
@@ -272,8 +274,8 @@ module Kobako
272
274
  # TrapError message so the failing export is identifiable.
273
275
  #
274
276
  # The yielded block must return a +Kobako::Snapshot+ — i.e. the
275
- # value of +Runtime#eval+ / +#run+ (SPEC.md Internal Concepts
276
- # Snapshot). The success path unpacks +#stdout+ / +#stderr+ into
277
+ # value of +Runtime#eval+ / +#run+. The success path unpacks
278
+ # +#stdout+ / +#stderr+ into
277
279
  # +Capture+ and feeds +#return_bytes+ to +Outcome.decode+; usage is
278
280
  # populated by the +ensure+ readout ({#read_usage!}) on every outcome.
279
281
  # The rescue chain is the single trap-translation boundary —
@@ -6,11 +6,13 @@ module Kobako
6
6
  # Data.define(...)+ subclass form (the Steep-friendly shape — see
7
7
  # +lib/kobako/outcome/panic.rb+).
8
8
  #
9
- # The +initialize+ method does double duty: it applies DEFAULT fallback
10
- # for absent values and normalises (timeout to Float seconds,
11
- # memory_limit to positive Integer bytes) before delegating to Data's
12
- # +super+. Anything that survives +SandboxOptions.new+ is a wire-ready
13
- # cap bundle the +Kobako::Runtime+ constructor consumes as-is.
9
+ # The +initialize+ normalises every cap before delegating to Data's
10
+ # +super+: +timeout+ to Float seconds, +memory_limit+ / +stdout_limit+ /
11
+ # +stderr_limit+ to positive Integer bytes. Each cap is +nil+-disablable
12
+ # (an absent argument takes its DEFAULT; an explicit +nil+ leaves the
13
+ # bound off), so all four behave uniformly. Anything that survives
14
+ # +SandboxOptions.new+ is a wire-ready cap bundle the +Kobako::Runtime+
15
+ # constructor consumes as-is.
14
16
  class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit)
15
17
  # Default wall-clock timeout for a single invocation: 60 seconds.
16
18
  DEFAULT_TIMEOUT_SECONDS = 60.0
@@ -25,12 +27,12 @@ module Kobako
25
27
 
26
28
  def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
27
29
  memory_limit: DEFAULT_MEMORY_LIMIT,
28
- stdout_limit: nil,
29
- stderr_limit: nil)
30
+ stdout_limit: DEFAULT_OUTPUT_LIMIT,
31
+ stderr_limit: DEFAULT_OUTPUT_LIMIT)
30
32
  timeout = normalize_timeout(timeout)
31
33
  memory_limit = normalize_memory_limit(memory_limit)
32
- stdout_limit ||= DEFAULT_OUTPUT_LIMIT
33
- stderr_limit ||= DEFAULT_OUTPUT_LIMIT
34
+ stdout_limit = normalize_output_limit(stdout_limit, "stdout_limit")
35
+ stderr_limit = normalize_output_limit(stderr_limit, "stderr_limit")
34
36
  super
35
37
  end
36
38
 
@@ -62,5 +64,19 @@ module Kobako
62
64
 
63
65
  memory_limit
64
66
  end
67
+
68
+ # Coerce a per-channel output cap (+stdout_limit+ / +stderr_limit+)
69
+ # into the byte cap the ext expects, or +nil+ to leave the channel
70
+ # uncapped. Same shape as +normalize_memory_limit+: a positive Integer
71
+ # when set, Float / zero / negative rejected. +name+ tags the
72
+ # +ArgumentError+ with the offending keyword.
73
+ def normalize_output_limit(limit, name)
74
+ return nil if limit.nil?
75
+ unless limit.is_a?(Integer) && limit.positive?
76
+ raise ArgumentError, "#{name} must be a positive Integer or nil, got #{limit.inspect}"
77
+ end
78
+
79
+ limit
80
+ end
65
81
  end
66
82
  end
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "capture"
4
- require_relative "usage"
5
4
 
6
5
  module Kobako
7
6
  # Kobako::Snapshot — per-invocation observable bundle returned from
8
7
  # +Kobako::Runtime#eval+ and +#run+.
9
8
  #
10
- # The magnus class (see ext/kobako/src/snapshot.rs) carries seven raw
9
+ # The magnus class (see ext/kobako/src/snapshot.rs) carries five raw
11
10
  # readers: +return_bytes+, +stdout_bytes+, +stdout_truncated+,
12
- # +stderr_bytes+, +stderr_truncated+, +wall_time+, +memory_peak+. This
13
- # file reopens the class to add the Ruby-side helpers that pack those
14
- # raw fields into the user-facing value objects +Kobako::Capture+ and
15
- # +Kobako::Usage+ the same shape +Kobako::Sandbox+ exposes to the
16
- # Host App.
11
+ # +stderr_bytes+, +stderr_truncated+. This file reopens the class to add
12
+ # the Ruby-side helpers that pack those raw fields into the user-facing
13
+ # value object +Kobako::Capture+ the same shape +Kobako::Sandbox+
14
+ # exposes to the Host App. Usage is not on the Snapshot; +Sandbox#usage+
15
+ # reads it from +Kobako::Runtime#usage+, which also covers the trap path
16
+ # where no Snapshot is produced.
17
17
  class Snapshot
18
18
  # Wrap the stdout capture pair (+stdout_bytes+, +stdout_truncated+)
19
19
  # as a +Kobako::Capture+ value object. The byte content never carries
@@ -28,11 +28,5 @@ module Kobako
28
28
  def stderr
29
29
  Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
30
30
  end
31
-
32
- # Wrap the per-last-invocation usage pair (+wall_time+,
33
- # +memory_peak+) as a +Kobako::Usage+ value object.
34
- def usage
35
- Usage.new(wall_time: wall_time, memory_peak: memory_peak)
36
- end
37
31
  end
38
32
  end
@@ -72,8 +72,8 @@ module Kobako
72
72
  # closure so the Dispatcher stays stateless and the registry doesn't
73
73
  # need to publish accessors for the Sandbox-owned +Catalog::Handles+
74
74
  # or +Runtime+. +yield_to_guest+ is a +String → String+ callable
75
- # (typically +Runtime#yield_to_active_invocation+ bound as a lambda)
76
- # used only when the Request carries +block_given: true+. Always
75
+ # (the ext's per-dispatch +Kobako::Runtime::GuestYielder+) used only
76
+ # when the Request carries +block_given: true+. Always
77
77
  # returns a binary String — every failure path is reified as a
78
78
  # Response.error envelope so the guest sees a transport error rather
79
79
  # than a wasm trap.
@@ -38,20 +38,6 @@ module Kobako
38
38
  new(status: STATUS_ERROR, payload: fault)
39
39
  end
40
40
 
41
- # Decode +bytes+ into a {Response}. Raises +Codec::InvalidType+ when the
42
- # envelope is not the expected 2-element msgpack array, or when the
43
- # Value Object's construction invariants reject the decoded fields.
44
- def self.decode(bytes)
45
- Codec::Decoder.decode(bytes) do |arr|
46
- unless arr.is_a?(Array) && arr.length == 2
47
- raise Codec::InvalidType, "Response envelope is malformed (expected a 2-element array)"
48
- end
49
-
50
- status, payload = arr
51
- new(status: status, payload: payload)
52
- end
53
- end
54
-
55
41
  def initialize(status:, payload:)
56
42
  unless [STATUS_OK, STATUS_ERROR].include?(status)
57
43
  raise ArgumentError, "Response status must be 0 (ok) or 1 (error), got #{status.inspect}"
@@ -71,6 +57,20 @@ module Kobako
71
57
  def encode
72
58
  Codec::Encoder.encode([status, payload])
73
59
  end
60
+
61
+ # Decode +bytes+ into a {Response}. Raises +Codec::InvalidType+ when the
62
+ # envelope is not the expected 2-element msgpack array, or when the
63
+ # Value Object's construction invariants reject the decoded fields.
64
+ def self.decode(bytes)
65
+ Codec::Decoder.decode(bytes) do |arr|
66
+ unless arr.is_a?(Array) && arr.length == 2
67
+ raise Codec::InvalidType, "Response envelope is malformed (expected a 2-element array)"
68
+ end
69
+
70
+ status, payload = arr
71
+ new(status: status, payload: payload)
72
+ end
73
+ end
74
74
  end
75
75
  end
76
76
  end
@@ -43,8 +43,8 @@ module Kobako
43
43
 
44
44
  def initialize(entrypoint:, args: [], kwargs: {})
45
45
  entrypoint = normalize_entrypoint(entrypoint)
46
- args = validate_args!(args)
47
- kwargs = validate_kwargs!(kwargs)
46
+ validate_args!(args)
47
+ validate_kwargs!(kwargs)
48
48
  super
49
49
  end
50
50
 
@@ -100,8 +100,6 @@ module Kobako
100
100
  def validate_args!(args)
101
101
  raise ArgumentError, "arguments must be an Array" unless args.is_a?(Array)
102
102
  raise ArgumentError, forged_handle_message("arguments") if args.any?(Kobako::Handle)
103
-
104
- args
105
103
  end
106
104
 
107
105
  # Reject a non-Symbol kwargs key, and a +Kobako::Handle+ arriving
@@ -117,8 +115,6 @@ module Kobako
117
115
  "keyword argument keys must be Symbols (got #{bad_keys.inspect})"
118
116
  end
119
117
  raise ArgumentError, forged_handle_message("keyword argument values") if kwargs.each_value.any?(Kobako::Handle)
120
-
121
- kwargs
122
118
  end
123
119
 
124
120
  # Single source of truth for the forged-Handle reject message so the
@@ -67,7 +67,7 @@ module Kobako
67
67
  raise Codec::InvalidType, "YieldResponse must carry at least one byte" if bytes.empty?
68
68
 
69
69
  tag = bytes.getbyte(0) # : Integer
70
- body = bytes.byteslice(1, bytes.bytesize - 1) || +""
70
+ body = bytes.byteslice(1, bytes.bytesize - 1) # : String
71
71
 
72
72
  reject_dead_tag!(tag)
73
73
  new(tag: tag, value: Codec::Decoder.decode(body))
@@ -28,8 +28,8 @@ module Kobako
28
28
  # dispatch completes; any later call to a stashed Yielder then raises
29
29
  # +LocalJumpError+ — the observable shape of an escaped Yielder.
30
30
  class Yielder
31
- # +yield_to_guest+ is a +String → String+ callable (typically
32
- # +Runtime#yield_to_active_invocation+ bound through a lambda) that
31
+ # +yield_to_guest+ is a +String → String+ callable (the ext's
32
+ # per-dispatch +Kobako::Runtime::GuestYielder+) that
33
33
  # {#yield} invokes to re-enter the guest; +break_tag+ is the +catch+
34
34
  # throw tag the Dispatcher matches against to unwind the Service on
35
35
  # +tag 0x02+. +handler+ is the Sandbox's +Kobako::Catalog::Handles+,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- VERSION = "0.12.1"
4
+ VERSION = "0.12.2"
5
5
  end
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
3
  "release-type": "ruby",
4
- "last-release-sha": "5694da60b08931ea260e13025689b8d8c47d767a",
4
+ "last-release-sha": "98509af508988f708bf0d7a76a718bb0428a177e",
5
5
  "packages": {
6
6
  ".": {
7
7
  "component": "kobako",
@@ -103,13 +103,58 @@
103
103
  "path": "/wasm/kobako-baker/README.md"
104
104
  }
105
105
  ]
106
+ },
107
+ "crates/kobako-runtime": {
108
+ "component": "kobako-runtime",
109
+ "release-type": "rust",
110
+ "extra-files": [
111
+ {
112
+ "type": "toml",
113
+ "path": "/crates/Cargo.lock",
114
+ "jsonpath": "$.package[?(@.name=='kobako-runtime')].version"
115
+ },
116
+ {
117
+ "type": "toml",
118
+ "path": "/Cargo.lock",
119
+ "jsonpath": "$.package[?(@.name=='kobako-runtime')].version"
120
+ },
121
+ {
122
+ "type": "generic",
123
+ "path": "/crates/kobako-runtime/README.md"
124
+ }
125
+ ]
126
+ },
127
+ "crates/kobako-wasmtime": {
128
+ "component": "kobako-wasmtime",
129
+ "release-type": "rust",
130
+ "extra-files": [
131
+ {
132
+ "type": "toml",
133
+ "path": "/crates/Cargo.lock",
134
+ "jsonpath": "$.package[?(@.name=='kobako-wasmtime')].version"
135
+ },
136
+ {
137
+ "type": "toml",
138
+ "path": "/Cargo.lock",
139
+ "jsonpath": "$.package[?(@.name=='kobako-wasmtime')].version"
140
+ },
141
+ {
142
+ "type": "toml",
143
+ "path": "/crates/kobako-wasmtime/Cargo.toml",
144
+ "jsonpath": "$.dependencies['kobako-runtime'].version"
145
+ },
146
+ {
147
+ "type": "generic",
148
+ "path": "/crates/kobako-wasmtime/README.md"
149
+ }
150
+ ]
106
151
  }
107
152
  },
108
153
  "plugins": [
109
154
  {
110
155
  "type": "linked-versions",
111
- "groupName": "kobako guest crates",
112
- "components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-json", "kobako-regexp", "kobako-baker"]
156
+ "groupName": "kobako crates",
157
+ "components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-json", "kobako-regexp", "kobako-baker", "kobako-runtime", "kobako-wasmtime"]
113
158
  }
114
159
  ],
115
160
  "extra-files": [
@@ -8,6 +8,8 @@ module Kobako
8
8
  EXT_SYMBOL: Integer
9
9
  EXT_HANDLE: Integer
10
10
  EXT_ERRENV: Integer
11
+ MAX_EXT_DEPTH: Integer
12
+ EXT_DEPTH_KEY: Symbol
11
13
 
12
14
  def dump: (untyped value) -> String
13
15
  def load: (String bytes) -> untyped
@@ -26,6 +28,7 @@ module Kobako
26
28
  def unpack_handle: (String payload) -> Kobako::Handle
27
29
  def pack_fault: (Kobako::Fault fault) -> String
28
30
  def unpack_fault: (String payload) -> Kobako::Fault
31
+ def within_ext_frame: [T] (singleton(Kobako::Codec::Error) over_limit) { () -> T } -> T
29
32
  end
30
33
  end
31
34
  end
@@ -17,7 +17,7 @@ module Kobako
17
17
  class ModuleNotBuiltError < SetupError
18
18
  end
19
19
 
20
- class SandboxError < Error
20
+ module StructuredError : Error
21
21
  attr_reader origin: String?
22
22
  attr_reader klass: String?
23
23
  attr_reader backtrace_lines: Array[String]?
@@ -32,22 +32,15 @@ module Kobako
32
32
  ) -> void
33
33
  end
34
34
 
35
- class ServiceError < Error
36
- attr_reader origin: String?
37
- attr_reader klass: String?
38
- attr_reader backtrace_lines: Array[String]?
39
- attr_reader details: untyped
35
+ class SandboxError < Error
36
+ include StructuredError
37
+ end
40
38
 
41
- def initialize: (
42
- String message,
43
- ?origin: String?,
44
- ?klass: String?,
45
- ?backtrace_lines: Array[String]?,
46
- ?details: untyped
47
- ) -> void
39
+ class ServiceError < Error
40
+ include StructuredError
48
41
  end
49
42
 
50
- class HandlerExhaustedError < SandboxError
43
+ class HandleExhaustedError < SandboxError
51
44
  end
52
45
 
53
46
  class BytecodeError < SandboxError
@@ -10,14 +10,19 @@ module Kobako
10
10
  Integer? stderr_limit_bytes
11
11
  ) -> Runtime
12
12
 
13
- def on_dispatch=: (^(String) -> String dispatch_proc) -> void
14
-
15
- def yield_to_active_invocation: (String args_bytes) -> String
13
+ def on_dispatch=: (^(String, Kobako::Transport::_GuestYielder) -> String dispatch_proc) -> void
16
14
 
17
15
  def eval: (String preamble, String source, String snippets) -> Kobako::Snapshot
18
16
 
19
17
  def run: (String preamble, String snippets, String envelope) -> Kobako::Snapshot
20
18
 
21
19
  def usage: () -> [Float, Integer]
20
+
21
+ # Frame-scoped guest re-entry handle the ext hands the dispatch Proc as
22
+ # its second argument; stands in as the +String -> String+ callable the
23
+ # host +Transport::Yielder+ invokes for a block yield.
24
+ class GuestYielder
25
+ def call: (String args_bytes) -> String
26
+ end
22
27
  end
23
28
  end
@@ -8,8 +8,8 @@ module Kobako
8
8
  # Forwarded to @options via Forwardable#def_delegators.
9
9
  def timeout: () -> Float?
10
10
  def memory_limit: () -> Integer?
11
- def stdout_limit: () -> Integer
12
- def stderr_limit: () -> Integer
11
+ def stdout_limit: () -> Integer?
12
+ def stderr_limit: () -> Integer?
13
13
 
14
14
  def initialize: (
15
15
  ?wasm_path: String?,
@@ -6,8 +6,8 @@ module Kobako
6
6
 
7
7
  attr_reader timeout: Float?
8
8
  attr_reader memory_limit: Integer?
9
- attr_reader stdout_limit: Integer
10
- attr_reader stderr_limit: Integer
9
+ attr_reader stdout_limit: Integer?
10
+ attr_reader stderr_limit: Integer?
11
11
 
12
12
  def self.new: (
13
13
  ?timeout: (Float | Integer)?,
@@ -28,5 +28,7 @@ module Kobako
28
28
  def normalize_timeout: ((Float | Integer)? timeout) -> Float?
29
29
 
30
30
  def normalize_memory_limit: (Integer? memory_limit) -> Integer?
31
+
32
+ def normalize_output_limit: (Integer? limit, String name) -> Integer?
31
33
  end
32
34
  end
@@ -5,11 +5,8 @@ module Kobako
5
5
  def stdout_truncated: () -> bool
6
6
  def stderr_bytes: () -> String
7
7
  def stderr_truncated: () -> bool
8
- def wall_time: () -> Float
9
- def memory_peak: () -> Integer
10
8
 
11
9
  def stdout: () -> Kobako::Capture
12
10
  def stderr: () -> Kobako::Capture
13
- def usage: () -> Kobako::Usage
14
11
  end
15
12
  end
@@ -12,7 +12,7 @@ module Kobako
12
12
 
13
13
  CALLABLE_ALLOW: Array[Symbol]
14
14
 
15
- def self?.dispatch: (String request_bytes, Kobako::Catalog::Namespaces namespaces, Kobako::Catalog::Handles handler, ^(String) -> String yield_to_guest) -> String
15
+ def self?.dispatch: (String request_bytes, Kobako::Catalog::Namespaces namespaces, Kobako::Catalog::Handles handler, Kobako::Transport::_GuestYielder yield_to_guest) -> String
16
16
 
17
17
  def self?.resolve_call_args: (Kobako::Transport::Request request, Kobako::Catalog::Handles handler) -> [Array[untyped], Hash[Symbol, untyped]]
18
18
 
@@ -17,9 +17,9 @@ module Kobako
17
17
 
18
18
  def normalize_entrypoint: (Symbol | String target) -> Symbol
19
19
 
20
- def validate_args!: (Array[untyped] args) -> Array[untyped]
20
+ def validate_args!: (Array[untyped] args) -> void
21
21
 
22
- def validate_kwargs!: (Hash[untyped, untyped] kwargs) -> Hash[Symbol, untyped]
22
+ def validate_kwargs!: (Hash[untyped, untyped] kwargs) -> void
23
23
 
24
24
  def forged_handle_message: (String slot) -> String
25
25
  end
@@ -1,12 +1,12 @@
1
1
  module Kobako
2
2
  module Transport
3
3
  class Yielder
4
- @yield_to_guest: ^(String) -> String
4
+ @yield_to_guest: Kobako::Transport::_GuestYielder
5
5
  @break_tag: Symbol
6
6
  @handler: Kobako::Catalog::Handles
7
7
  @active: bool
8
8
 
9
- def initialize: (^(String) -> String yield_to_guest, Symbol break_tag, Kobako::Catalog::Handles handler) -> void
9
+ def initialize: (Kobako::Transport::_GuestYielder yield_to_guest, Symbol break_tag, Kobako::Catalog::Handles handler) -> void
10
10
 
11
11
  def yield: (*untyped args) -> untyped
12
12
 
@@ -1,4 +1,12 @@
1
1
  module Kobako
2
2
  module Transport
3
+ # The guest re-entry callable a dispatch hands to a Yielder: invoked
4
+ # with the encoded yield args, it returns the encoded YieldResponse.
5
+ # The ext's Kobako::Runtime::GuestYielder is the production conformer;
6
+ # modelled structurally so the Transport layer needs no upward
7
+ # dependency on Runtime.
8
+ interface _GuestYielder
9
+ def call: (String) -> String
10
+ end
3
11
  end
4
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kobako
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.12.2
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Aotokitsuruya
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-27 00:00:00.000000000 Z
11
+ date: 2026-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -38,6 +38,10 @@ files:
38
38
  - LICENSE
39
39
  - README.md
40
40
  - SECURITY.md
41
+ - crates/kobako-runtime/CHANGELOG.md
42
+ - crates/kobako-runtime/README.md
43
+ - crates/kobako-wasmtime/CHANGELOG.md
44
+ - crates/kobako-wasmtime/README.md
41
45
  - data/kobako.wasm
42
46
  - lib/kobako.rb
43
47
  - lib/kobako/3.3/kobako.so