kobako 0.4.0 → 0.5.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -0
  3. data/CHANGELOG.md +29 -0
  4. data/Cargo.lock +1 -1
  5. data/README.md +0 -1
  6. data/data/kobako.wasm +0 -0
  7. data/ext/kobako/Cargo.toml +1 -1
  8. data/ext/kobako/src/lib.rs +4 -2
  9. data/ext/kobako/src/{wasm → runtime}/cache.rs +12 -16
  10. data/ext/kobako/src/runtime/capture.rs +91 -0
  11. data/ext/kobako/src/runtime/config.rs +26 -0
  12. data/ext/kobako/src/runtime/dispatch.rs +211 -0
  13. data/ext/kobako/src/runtime/exports.rs +51 -0
  14. data/ext/kobako/src/runtime/guest_mem.rs +228 -0
  15. data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +94 -86
  16. data/ext/kobako/src/runtime/trap.rs +134 -0
  17. data/ext/kobako/src/runtime.rs +782 -0
  18. data/ext/kobako/src/snapshot.rs +110 -0
  19. data/lib/kobako/capture.rb +11 -16
  20. data/lib/kobako/catalog/handles.rb +107 -0
  21. data/lib/kobako/catalog/namespaces.rb +99 -0
  22. data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
  23. data/lib/kobako/catalog.rb +18 -0
  24. data/lib/kobako/codec/decoder.rb +13 -5
  25. data/lib/kobako/codec/factory.rb +12 -12
  26. data/lib/kobako/codec/utils.rb +56 -59
  27. data/lib/kobako/codec.rb +6 -3
  28. data/lib/kobako/errors.rb +45 -28
  29. data/lib/kobako/fault.rb +40 -0
  30. data/lib/kobako/handle.rb +4 -6
  31. data/lib/kobako/namespace.rb +67 -0
  32. data/lib/kobako/outcome.rb +31 -35
  33. data/lib/kobako/runtime.rb +30 -0
  34. data/lib/kobako/sandbox.rb +83 -72
  35. data/lib/kobako/sandbox_options.rb +6 -9
  36. data/lib/kobako/snapshot.rb +40 -0
  37. data/lib/kobako/snippet/binary.rb +6 -7
  38. data/lib/kobako/snippet/source.rb +8 -8
  39. data/lib/kobako/snippet.rb +7 -9
  40. data/lib/kobako/transport/dispatcher.rb +195 -0
  41. data/lib/kobako/{rpc/wire_error.rb → transport/error.rb} +7 -6
  42. data/lib/kobako/transport/request.rb +78 -0
  43. data/lib/kobako/transport/response.rb +69 -0
  44. data/lib/kobako/transport/run.rb +141 -0
  45. data/lib/kobako/transport/yield.rb +91 -0
  46. data/lib/kobako/transport/yielder.rb +89 -0
  47. data/lib/kobako/transport.rb +24 -0
  48. data/lib/kobako/version.rb +1 -1
  49. data/lib/kobako.rb +4 -4
  50. data/release-please-config.json +24 -0
  51. data/sig/kobako/capture.rbs +0 -2
  52. data/sig/kobako/catalog/handles.rbs +19 -0
  53. data/sig/kobako/catalog/namespaces.rbs +17 -0
  54. data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
  55. data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
  56. data/sig/kobako/codec/decoder.rbs +2 -1
  57. data/sig/kobako/codec/factory.rbs +2 -2
  58. data/sig/kobako/codec/utils.rbs +5 -5
  59. data/sig/kobako/errors.rbs +7 -7
  60. data/sig/kobako/fault.rbs +19 -0
  61. data/sig/kobako/handle.rbs +2 -3
  62. data/sig/kobako/namespace.rbs +19 -0
  63. data/sig/kobako/outcome.rbs +2 -2
  64. data/sig/kobako/runtime.rbs +23 -0
  65. data/sig/kobako/sandbox.rbs +5 -8
  66. data/sig/kobako/snapshot.rbs +15 -0
  67. data/sig/kobako/transport/dispatcher.rbs +34 -0
  68. data/sig/kobako/transport/error.rbs +6 -0
  69. data/sig/kobako/transport/request.rbs +32 -0
  70. data/sig/kobako/transport/response.rbs +30 -0
  71. data/sig/kobako/transport/run.rbs +27 -0
  72. data/sig/kobako/transport/yield.rbs +34 -0
  73. data/sig/kobako/transport/yielder.rbs +21 -0
  74. data/sig/kobako/transport.rbs +4 -0
  75. metadata +48 -30
  76. data/ext/kobako/src/wasm/dispatch.rs +0 -162
  77. data/ext/kobako/src/wasm/instance.rs +0 -873
  78. data/ext/kobako/src/wasm.rs +0 -126
  79. data/lib/kobako/handle_table.rb +0 -119
  80. data/lib/kobako/invocation.rb +0 -143
  81. data/lib/kobako/rpc/dispatcher.rb +0 -171
  82. data/lib/kobako/rpc/envelope.rb +0 -118
  83. data/lib/kobako/rpc/fault.rb +0 -41
  84. data/lib/kobako/rpc/namespace.rb +0 -74
  85. data/lib/kobako/rpc/server.rb +0 -146
  86. data/lib/kobako/rpc.rb +0 -11
  87. data/lib/kobako/wasm.rb +0 -25
  88. data/sig/kobako/handle_table.rbs +0 -23
  89. data/sig/kobako/invocation.rbs +0 -25
  90. data/sig/kobako/rpc/dispatcher.rbs +0 -33
  91. data/sig/kobako/rpc/envelope.rbs +0 -51
  92. data/sig/kobako/rpc/fault.rbs +0 -20
  93. data/sig/kobako/rpc/namespace.rbs +0 -24
  94. data/sig/kobako/rpc/server.rbs +0 -31
  95. data/sig/kobako/rpc/wire_error.rbs +0 -6
  96. data/sig/kobako/wasm.rbs +0 -41
@@ -4,28 +4,27 @@ require "forwardable"
4
4
 
5
5
  require_relative "capture"
6
6
  require_relative "errors"
7
- require_relative "handle_table"
8
- require_relative "invocation"
9
7
  require_relative "outcome"
10
- require_relative "rpc/server"
11
- require_relative "rpc/envelope"
12
8
  require_relative "sandbox_options"
13
- require_relative "snippet"
14
9
  require_relative "usage"
10
+ require_relative "transport"
11
+ require_relative "catalog"
15
12
 
16
13
  module Kobako
17
14
  # Kobako::Sandbox — the user-facing entry point for executing guest mruby
18
15
  # scripts inside a wasmtime-hosted Wasm module
19
16
  # ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
20
17
  #
21
- # The Sandbox owns the +Kobako::Wasm::Instance+, the per-Sandbox
22
- # +Kobako::HandleTable+ ({docs/behavior.md B-19}[link:../../docs/behavior.md]),
23
- # the per-instance RPC Server (which receives the HandleTable by
24
- # injection so guest→host dispatch and host→guest auto-wrap share one
25
- # allocator), and the per-channel byte caches for guest stdout / stderr
26
- # capture. The underlying wasmtime Engine and compiled Module are cached
27
- # at process scope by the native ext and never surface to Ruby —
28
- # constructing many Sandboxes amortises both costs automatically.
18
+ # The Sandbox owns the +Kobako::Runtime+, the per-Sandbox
19
+ # +Kobako::Catalog::Handles+ ({docs/behavior.md B-19}[link:../../docs/behavior.md]),
20
+ # the per-instance +Kobako::Catalog::Namespaces+ (which receives the
21
+ # +Catalog::Handles+ by injection so guest→host dispatch and host→guest
22
+ # auto-wrap share one allocator), and the dispatch +Proc+ /
23
+ # +yield_to_guest+ lambda installed on the Runtime via
24
+ # +Runtime#on_dispatch=+ ({docs/behavior.md B-12}[link:../../docs/behavior.md]).
25
+ # The underlying wasmtime Engine and compiled Module are cached at process
26
+ # scope by the native ext and never surface to Ruby — constructing many
27
+ # Sandboxes amortises both costs automatically.
29
28
  #
30
29
  # Output capture policy ({docs/behavior.md B-04}[link:../../docs/behavior.md]): the
31
30
  # per-channel cap (+stdout_limit+ / +stderr_limit+) is enforced inside the
@@ -38,9 +37,7 @@ module Kobako
38
37
  class Sandbox
39
38
  extend Forwardable
40
39
 
41
- attr_reader :wasm_path, :instance,
42
- :options,
43
- :services, :snippets
40
+ attr_reader :wasm_path, :options
44
41
 
45
42
  # Per-cap accessors forward to the immutable +SandboxOptions+ Value
46
43
  # Object so the Host App still reads them off Sandbox directly.
@@ -99,21 +96,22 @@ module Kobako
99
96
  def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
100
97
  timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
101
98
  memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
102
- @wasm_path = wasm_path || Kobako::Wasm.default_path
99
+ @wasm_path = wasm_path || Kobako::Runtime.default_path
103
100
  @options = SandboxOptions.new(timeout: timeout, memory_limit: memory_limit, stdout_limit: stdout_limit,
104
101
  stderr_limit: stderr_limit)
105
- @handle_table = HandleTable.new
106
- @services = Kobako::RPC::Server.new(handle_table: @handle_table)
107
- @snippets = Snippet::Table.new
108
- @instance = Kobako::Wasm::Instance.from_path(@wasm_path, @options.timeout, @options.memory_limit,
109
- @options.stdout_limit, @options.stderr_limit)
110
- @instance.server = @services
102
+ @handler = Catalog::Handles.new
103
+ @services = Kobako::Catalog::Namespaces.new(handler: @handler)
104
+ @snippets = Catalog::Snippets.new
105
+ @runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
106
+ @options.stdout_limit, @options.stderr_limit)
107
+ install_dispatch_proc!
111
108
  reset_invocation_state!
112
109
  end
113
110
 
114
111
  # Declare or retrieve the Namespace named +name+ on this Sandbox
115
112
  # ({docs/behavior.md B-07, B-09, B-10}[link:../../docs/behavior.md]). +name+ must be a
116
- # Symbol or String in constant form. Returns the +Kobako::RPC::Namespace+.
113
+ # Symbol or String in constant form. Returns the
114
+ # +Kobako::Namespace+.
117
115
  #
118
116
  # Raises +ArgumentError+ when called after the first invocation, or
119
117
  # when +name+ does not match the constant-name pattern.
@@ -160,17 +158,19 @@ module Kobako
160
158
 
161
159
  # Dispatch into a preloaded entrypoint constant
162
160
  # ({docs/behavior.md B-31}[link:../../docs/behavior.md]). Delegates host
163
- # pre-flight (E-24 / E-25 / E-29 / E-30) and wire encoding to
164
- # +Kobako::Invocation+ / +Kobako::Invocation#encode+; the guest
165
- # resolves +target+ as a top-level constant, calls +#call+ on it
166
- # with +args+ / +kwargs+, and returns the deserialized result. The
167
- # first invocation seals the Service registry and snippet table
168
- # (B-07 / B-33). Runtime errors follow the same three-class taxonomy
169
- # as +#eval+.
161
+ # pre-flight and wire encoding to +Kobako::Transport::Run+ /
162
+ # +Kobako::Transport::Run#encode+: a non-Symbol/String +target+ raises
163
+ # +TypeError+ (E-24), while a +target+ failing the constant pattern
164
+ # (E-25), a forged +Kobako::Handle+ in +args+ / +kwargs+ (E-29), or a
165
+ # non-Symbol +kwargs+ key (E-30) raise +ArgumentError+. The guest
166
+ # resolves +target+ as a top-level constant, calls +#call+ on it with
167
+ # +args+ / +kwargs+, and returns the deserialized result. The first
168
+ # invocation seals the Service registry and snippet table (B-07 /
169
+ # B-33). Runtime errors follow the same three-class taxonomy as +#eval+.
170
170
  def run(target, *args, **kwargs)
171
- invocation = Invocation.new(entrypoint: target, args: args, kwargs: kwargs)
171
+ run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
172
172
  invoke!(:run) do
173
- @instance.run(@services.encoded_preamble, @snippets.encode, invocation.encode(@handle_table))
173
+ @runtime.run(@services.encode, @snippets.encode, run_envelope.encode(@handler))
174
174
  end
175
175
  end
176
176
 
@@ -201,23 +201,38 @@ module Kobako
201
201
  raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
202
202
 
203
203
  invoke!(:eval) do
204
- @instance.eval(@services.encoded_preamble, code.b, @snippets.encode)
204
+ @runtime.eval(@services.encode, code.b, @snippets.encode)
205
205
  end
206
206
  end
207
207
 
208
208
  private
209
209
 
210
+ # Configure the +Runtime+'s host↔guest dispatch wiring
211
+ # ({docs/behavior.md B-12}[link:../../docs/behavior.md]). Builds a
212
+ # lambda that re-enters the guest via
213
+ # +Runtime#yield_to_active_invocation+ (B-24) and a dispatch +Proc+
214
+ # that routes guest→host calls through the stateless
215
+ # +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
216
+ # closure. Both are registered on the +Runtime+ once at construction
217
+ # time so the wasm ext callback can fire without further setup.
218
+ def install_dispatch_proc!
219
+ yield_to_guest = ->(args_bytes) { @runtime.yield_to_active_invocation(args_bytes) }
220
+ @runtime.on_dispatch = lambda do |request_bytes|
221
+ Transport::Dispatcher.dispatch(request_bytes, @services, @handler, yield_to_guest)
222
+ end
223
+ end
224
+
210
225
  # Per-invocation prologue ({docs/behavior.md B-03 / B-07 /
211
226
  # B-33}[link:../../docs/behavior.md]). Seals the Service / snippet
212
227
  # registries on first call (idempotent) and zeros the per-invocation
213
228
  # capability state — capture buffers, truncation predicates, and the
214
- # HandleTable counter — before the guest runs. The HandleTable
215
- # itself is held as +@handle_table+ and never exposed beyond
229
+ # +Catalog::Handles+ counter — before the guest runs. The
230
+ # +Catalog::Handles+ itself is held as +@handler+ and never exposed beyond
216
231
  # this class: SPEC.md Terminology pins it as "Not exposed to the
217
232
  # Host App" (B-19 / B-20 / E-29).
218
233
  def begin_invocation!
219
234
  @services.seal!
220
- @handle_table.reset!
235
+ @handler.reset!
221
236
  reset_invocation_state!
222
237
  end
223
238
 
@@ -235,66 +250,62 @@ module Kobako
235
250
  @usage = Usage::EMPTY
236
251
  end
237
252
 
238
- # Read the per-channel capture pairs (+[bytes, truncated]+) from the
239
- # ext after an invocation completes and wrap each as a +Capture+ value
240
- # object. The ext clips +bytes+ to the configured cap and sets
241
- # +truncated+ when the guest produced strictly more than +cap+ bytes
242
- # ({docs/behavior.md B-04}[link:../../docs/behavior.md]). Mirror of
243
- # {#reset_invocation_state!} at the post-invocation boundary.
244
- def read_captures!
245
- out_bytes, out_truncated = @instance.stdout
246
- err_bytes, err_truncated = @instance.stderr
247
- @stdout_capture = Capture.from_ext(out_bytes, out_truncated)
248
- @stderr_capture = Capture.from_ext(err_bytes, err_truncated)
249
- end
250
-
251
253
  # Read the per-last-invocation +wall_time+ and +memory_peak+ from
252
254
  # the ext and wrap them as a +Kobako::Usage+ value object
253
255
  # ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
254
256
  # the +invoke!+ +ensure+ block so the usage record is populated on
255
257
  # every outcome — value return, +Kobako::TrapError+ (including
256
258
  # +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
257
- # and +Kobako::ServiceError+.
259
+ # and +Kobako::ServiceError+. On the success path the same figures
260
+ # already arrive via +Snapshot#usage+; on the trap path the Snapshot
261
+ # never reaches Ruby so the ext readout here is the only source.
258
262
  #
259
263
  # The ext returns a positional 2-tuple +[wall_time, memory_peak]+
260
264
  # whose order matches the +Kobako::Usage+ field order; the
261
265
  # destructure-then-kwargs handoff below is the explicit
262
- # positional→keyword conversion point, mirroring
263
- # +#read_captures!+'s +Capture.from_ext(bytes, truncated)+ shape.
266
+ # positional→keyword conversion point.
264
267
  def read_usage!
265
- wall_time, memory_peak = @instance.usage
268
+ wall_time, memory_peak = @runtime.usage
266
269
  @usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak)
267
270
  end
268
271
 
269
- # Map a wasmtime trap class to the matching three-layer Ruby
270
- # exception class. Cap-trap subclasses
272
+ # Pick the +TrapError+ subclass to re-raise based on +err+'s actual
273
+ # class. Cap-trap subclasses
271
274
  # ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
272
- # select their named +TrapError+ subclass; everything else
273
- # collapses to the base +Kobako::TrapError+.
275
+ # preserve their named identity; everything else collapses to the
276
+ # base +Kobako::TrapError+. The ext already raises the right subclass
277
+ # directly, so this is a pure re-attribution that lets +#invoke!+
278
+ # add the verb prefix without erasing +TimeoutError+ /
279
+ # +MemoryLimitError+.
274
280
  def trap_class_for(err)
275
281
  case err
276
- when Kobako::Wasm::TimeoutError then TimeoutError
277
- when Kobako::Wasm::MemoryLimitError then MemoryLimitError
282
+ when TimeoutError then TimeoutError
283
+ when MemoryLimitError then MemoryLimitError
278
284
  else TrapError
279
285
  end
280
286
  end
281
287
 
282
288
  # Shared prologue / epilogue + trap-class translator for both
283
289
  # invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
284
- # TrapError message so the failing export is identifiable. The
285
- # rescue chain is the single trap-translation boundary — wasmtime /
286
- # wire failures from the guest call and from the subsequent
287
- # +Instance#outcome!+ read both flow through here, so an
288
- # OUTCOME_BUFFER read failure attributes to the same export name as
289
- # the guest call itself. Configured-cap paths
290
- # ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md]) surface as
291
- # named TrapError subclasses.
290
+ # TrapError message so the failing export is identifiable.
291
+ #
292
+ # The yielded block must return a +Kobako::Snapshot+ i.e. the
293
+ # value of +Runtime#eval+ / +#run+ (SPEC.md Internal Concepts
294
+ # Snapshot). The success path unpacks every observable from the
295
+ # Snapshot in one go: +#stdout+ / +#stderr+ pack into +Capture+,
296
+ # +#usage+ packs into +Usage+, +#return_bytes+ feeds +Outcome.decode+.
297
+ # The rescue chain is the single trap-translation boundary —
298
+ # configured-cap paths
299
+ # ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
300
+ # surface as named TrapError subclasses; everything else surfaces as
301
+ # the base +TrapError+.
292
302
  def invoke!(verb)
293
303
  begin_invocation!
294
- yield
295
- read_captures!
296
- Outcome.decode(@instance.outcome!)
297
- rescue Kobako::Wasm::Error => e
304
+ snapshot = yield
305
+ @stdout_capture = snapshot.stdout
306
+ @stderr_capture = snapshot.stderr
307
+ Outcome.decode(snapshot.return_bytes)
308
+ rescue Kobako::TrapError => e
298
309
  raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
299
310
  ensure
300
311
  read_usage!
@@ -11,7 +11,7 @@ module Kobako
11
11
  # for absent values and normalises (timeout to Float seconds,
12
12
  # memory_limit to positive Integer bytes) before delegating to Data's
13
13
  # +super+. Anything that survives +SandboxOptions.new+ is a wire-ready
14
- # cap bundle the +Kobako::Wasm::Instance+ constructor consumes as-is.
14
+ # cap bundle the +Kobako::Runtime+ constructor consumes as-is.
15
15
  class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit)
16
16
  # Default wall-clock timeout for a single invocation: 60 seconds
17
17
  # ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
@@ -27,17 +27,15 @@ module Kobako
27
27
  # ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
28
28
  DEFAULT_OUTPUT_LIMIT = 1 << 20
29
29
 
30
- # steep:ignore:start
31
30
  def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
32
31
  memory_limit: DEFAULT_MEMORY_LIMIT,
33
32
  stdout_limit: nil,
34
33
  stderr_limit: nil)
35
- super(
36
- timeout: normalize_timeout(timeout),
37
- memory_limit: normalize_memory_limit(memory_limit),
38
- stdout_limit: stdout_limit || DEFAULT_OUTPUT_LIMIT,
39
- stderr_limit: stderr_limit || DEFAULT_OUTPUT_LIMIT
40
- )
34
+ timeout = normalize_timeout(timeout)
35
+ memory_limit = normalize_memory_limit(memory_limit)
36
+ stdout_limit ||= DEFAULT_OUTPUT_LIMIT
37
+ stderr_limit ||= DEFAULT_OUTPUT_LIMIT
38
+ super
41
39
  end
42
40
 
43
41
  private
@@ -68,6 +66,5 @@ module Kobako
68
66
 
69
67
  memory_limit
70
68
  end
71
- # steep:ignore:end
72
69
  end
73
70
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "capture"
4
+ require_relative "usage"
5
+
6
+ module Kobako
7
+ # Kobako::Snapshot — per-invocation observable bundle returned from
8
+ # +Kobako::Runtime#eval+ and +#run+.
9
+ #
10
+ # The magnus class (see ext/kobako/src/snapshot.rs) carries seven raw
11
+ # 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.
17
+ class Snapshot
18
+ # Wrap the stdout capture pair (+stdout_bytes+, +stdout_truncated+)
19
+ # as a +Kobako::Capture+ value object. {docs/behavior.md
20
+ # B-04}[link:../../docs/behavior.md] — the byte content never carries
21
+ # a truncation sentinel; +#truncated?+ is the only way to observe
22
+ # that the cap was hit.
23
+ def stdout
24
+ Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
25
+ end
26
+
27
+ # Wrap the stderr capture pair as a +Kobako::Capture+ value object.
28
+ # Mirror of +#stdout+.
29
+ def stderr
30
+ Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
31
+ end
32
+
33
+ # Wrap the per-last-invocation usage pair (+wall_time+,
34
+ # +memory_peak+) as a +Kobako::Usage+ value object
35
+ # ({docs/behavior.md B-35}[link:../../docs/behavior.md]).
36
+ def usage
37
+ Usage.new(wall_time: wall_time, memory_peak: memory_peak)
38
+ end
39
+ end
40
+ end
@@ -3,23 +3,22 @@
3
3
  module Kobako
4
4
  module Snippet
5
5
  # Kobako::Snippet::Binary — value object representing a single
6
- # +#preload(binary:)+ entry held by +Kobako::Snippet::Table+
6
+ # +#preload(binary:)+ entry held by +Kobako::Catalog::Snippets+
7
7
  # ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
8
8
  #
9
9
  # The +body+ is RITE bytecode (as emitted by +mrbc+) carried as an
10
10
  # +ASCII_8BIT+ String so msgpack-ruby encodes it as +bin+ family on
11
11
  # the wire ({docs/wire-codec.md Invocation channels}[link:../../../docs/wire-codec.md]).
12
12
  # The host treats the bytes as opaque — the snippet's canonical
13
- # name, when present, lives in the bytecode's embedded
14
- # +debug_info+ and is resolved by the guest at load time;
15
- # structural validation
13
+ # name, when present, lives in the bytecode's embedded +debug_info+
14
+ # and is resolved by the guest at load time; structural validation
16
15
  # ({docs/behavior.md E-37 / E-38}[link:../../../docs/behavior.md])
17
16
  # is deferred to the first invocation's guest replay.
18
17
  #
19
18
  # The class is a +Data.define+ subclass — frozen and value-equal.
20
- # Callers (chiefly +Table+) construct instances via keyword form
21
- # +Binary.new(body: ...)+. Wire-form construction is the +Table+'s
22
- # responsibility.
19
+ # Callers (chiefly +Catalog::Snippets+) construct instances via keyword
20
+ # form +Binary.new(body: ...)+. Wire-form construction is the
21
+ # registry's responsibility.
23
22
  class Binary < Data.define(:body)
24
23
  # The +kind+ field value carried by bytecode snippets in their
25
24
  # Frame 3 wire envelope entry
@@ -3,21 +3,21 @@
3
3
  module Kobako
4
4
  module Snippet
5
5
  # Kobako::Snippet::Source — value object representing a single
6
- # +#preload(code:, name:)+ entry held by +Kobako::Snippet::Table+
6
+ # +#preload(code:, name:)+ entry held by +Kobako::Catalog::Snippets+
7
7
  # ({docs/behavior.md B-32}[link:../../../docs/behavior.md]).
8
8
  #
9
9
  # +name+ is the canonical +Symbol+ identity baked into the loaded
10
10
  # IREP's +debug_info+; backtrace frames originating in this snippet
11
11
  # surface as +(snippet:Name):line+. +body+ is the UTF-8 mruby source
12
- # detached from the caller's reference at +Table#register+ time so
13
- # later mutation of the original String cannot bleed through.
12
+ # detached from the caller's reference at +Catalog::Snippets#register+
13
+ # time so later mutation of the original String cannot bleed through.
14
14
  #
15
15
  # The class is a +Data.define+ subclass — frozen, value-equal, and
16
- # carries no mutation API. Callers (chiefly +Table+) construct
17
- # instances via keyword form +Source.new(name: ..., body: ...)+.
18
- # Wire-form construction is the +Table+'s responsibility, mirroring
19
- # +Kobako::RPC.encode_request+'s pattern of reading attributes off a
20
- # carrier rather than asking the carrier to self-describe.
16
+ # carries no mutation API. Callers (chiefly +Catalog::Snippets+)
17
+ # construct instances via keyword form +Source.new(name: ..., body: ...)+.
18
+ # Wire-form construction is the registry's responsibility: as a leaf
19
+ # carrier this Source stays pure and +Catalog::Snippets#encode+ reads
20
+ # its attributes off the outside rather than asking it to self-encode.
21
21
  class Source < Data.define(:name, :body)
22
22
  # The +kind+ field value carried by source snippets in their Frame
23
23
  # 3 wire envelope entry
@@ -2,19 +2,17 @@
2
2
 
3
3
  require_relative "snippet/binary"
4
4
  require_relative "snippet/source"
5
- require_relative "snippet/table"
6
5
 
7
6
  module Kobako
8
- # Kobako::Snippet — namespace for the per-Sandbox preloaded snippet
9
- # registry and its entry value objects
7
+ # Kobako::Snippet — value-object family for preloaded snippet entries
8
+ # held by +Kobako::Catalog::Snippets+
10
9
  # ({docs/behavior.md B-32 / B-33}[link:../../docs/behavior.md]).
11
10
  #
12
- # The +Table+ owns insertion-ordered storage and seal-coordination with
13
- # the owning Sandbox; +Source+ is the value object representing a single
14
- # +#preload(code:, name:)+ entry. Entry types live as siblings under
15
- # this module rather than nested under +Table+ so they remain plain
16
- # value objects with no implicit dependency on the registry that holds
17
- # them.
11
+ # +Source+ represents a single +#preload(code:, name:)+ entry; +Binary+
12
+ # represents a single +#preload(binary:)+ entry. Both are plain value
13
+ # objects with no dependency on the +Catalog::Snippets+ registry that
14
+ # holds them the registry reads their attributes externally when
15
+ # encoding the wire envelope.
18
16
  module Snippet
19
17
  end
20
18
  end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../codec"
4
+ require_relative "request"
5
+ require_relative "response"
6
+ require_relative "yield"
7
+ require_relative "yielder"
8
+
9
+ module Kobako
10
+ # See lib/kobako/transport.rb for the umbrella module doc; this file
11
+ # owns the pure-function dispatcher that decodes guest-initiated
12
+ # Requests and produces encoded Responses.
13
+ module Transport
14
+ # Pure-function dispatcher for guest-initiated transport calls.
15
+ # Decodes a msgpack-encoded Request envelope, resolves the target
16
+ # object through the Catalog::Namespaces (path lookup) or
17
+ # Catalog::Handles (Handle lookup), invokes the method, and returns
18
+ # a msgpack-encoded Response envelope.
19
+ #
20
+ # The module is stateless — all mutable state is threaded through
21
+ # arguments so Dispatcher has no instance variables and no side
22
+ # effects beyond mutating the Catalog::Handles via +alloc+ when a
23
+ # non-wire-representable return value must be wrapped
24
+ # ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
25
+ #
26
+ # Entry point:
27
+ #
28
+ # Kobako::Transport::Dispatcher.dispatch(request_bytes, namespaces, handler, yield_to_guest)
29
+ # # => msgpack-encoded Response bytes (never raises)
30
+ module Dispatcher
31
+ # Throw tag for the {Yielder}'s break unwind back to the
32
+ # dispatcher's +catch+ frame (B-25). +private_constant+ is a
33
+ # convention boundary — not a defence.
34
+ BREAK_THROW = :__kobako_break__
35
+ private_constant :BREAK_THROW
36
+
37
+ module_function
38
+
39
+ # Internal sentinel raised when target resolution fails. Mapped to
40
+ # Response.error with type="undefined". Contained at the wire boundary —
41
+ # not part of the public Kobako error taxonomy
42
+ # ({docs/behavior.md E-12}[link:../../../docs/behavior.md]).
43
+ class UndefinedTargetError < StandardError; end
44
+
45
+ # Dispatch a single transport request and return the encoded
46
+ # Response bytes ({docs/behavior.md B-12}[link:../../../docs/behavior.md]).
47
+ # Invoked from the +Runtime#on_dispatch+ Proc that
48
+ # +Kobako::Sandbox#initialize+ installs on the ext side; +namespaces+,
49
+ # +handler+, and +yield_to_guest+ are captured in that Proc's
50
+ # closure so the Dispatcher stays stateless and the registry doesn't
51
+ # need to publish accessors for the Sandbox-owned +Catalog::Handles+
52
+ # or +Runtime+. +yield_to_guest+ is a +String → String+ callable
53
+ # (typically +Runtime#yield_to_active_invocation+ bound as a lambda)
54
+ # used only when the Request carries +block_given: true+. Always
55
+ # returns a binary String — every failure path is reified as a
56
+ # Response.error envelope so the guest sees a transport error rather
57
+ # than a wasm trap.
58
+ def dispatch(request_bytes, namespaces, handler, yield_to_guest)
59
+ request = Kobako::Transport::Request.decode(request_bytes)
60
+ target = resolve_target(request.target, namespaces, handler)
61
+ args, kwargs = resolve_call_args(request, handler)
62
+ yielder = Yielder.new(yield_to_guest, BREAK_THROW) if request.block_given
63
+ value = catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }
64
+ encode_ok(value, handler)
65
+ rescue StandardError => e
66
+ encode_caught_error(e)
67
+ ensure
68
+ yielder&.invalidate!
69
+ end
70
+
71
+ # Resolve positional and keyword arguments off +request+ in one
72
+ # step. Both pass through {#resolve_arg} so Capability Handles
73
+ # round-trip back to the host-side Ruby object before the call
74
+ # reaches +public_send+.
75
+ def resolve_call_args(request, handler)
76
+ args = request.args.map { |v| resolve_arg(v, handler) }
77
+ kwargs = request.kwargs.transform_values { |v| resolve_arg(v, handler) }
78
+ [args, kwargs]
79
+ end
80
+
81
+ # Map an error caught at the dispatch boundary to a +Response.error+
82
+ # envelope. +error+ is the +StandardError+ caught by {#dispatch}'s
83
+ # rescue. Returns a msgpack-encoded Response envelope (binary). Three
84
+ # error buckets ({docs/behavior.md B-12}[link:../../../docs/behavior.md]):
85
+ # +Kobako::Codec::Error+ → type="runtime" (malformed request);
86
+ # +UndefinedTargetError+ → type="undefined" (E-13); +ArgumentError+ →
87
+ # type="argument" (B-12 arity mismatch); everything else →
88
+ # type="runtime".
89
+ def encode_caught_error(error)
90
+ case error
91
+ when Kobako::Codec::Error then encode_error("runtime",
92
+ "Sandbox received a malformed request: #{error.message}")
93
+ when UndefinedTargetError then encode_error("undefined", error.message)
94
+ when ArgumentError then encode_error("argument", error.message)
95
+ else encode_error("runtime", "#{error.class}: #{error.message}")
96
+ end
97
+ end
98
+
99
+ # Dispatch +method+ on +target+. +kwargs+ is already Symbol-keyed
100
+ # (the +Request+ invariant pins it). The empty-kwargs branch omits
101
+ # the +**+ splat so Ruby 3.x's strict kwargs separation does not
102
+ # reject calls to no-kwarg methods when the wire carries the
103
+ # uniform empty-map shape.
104
+ #
105
+ # +yielder+ is the host-side {Yielder} materialised when the guest
106
+ # call site supplied a block ({docs/behavior.md
107
+ # B-23}[link:../../../docs/behavior.md]); its {Yielder#to_proc}
108
+ # rides the +&block+ slot. +&nil+ is a no-op block argument in Ruby,
109
+ # so the same call site handles both cases without an explicit
110
+ # conditional.
111
+ def invoke(target, method, args, kwargs, yielder = nil)
112
+ block = yielder&.to_proc
113
+ if kwargs.empty?
114
+ target.public_send(method.to_sym, *args, &block)
115
+ else
116
+ target.public_send(method.to_sym, *args, **kwargs, &block)
117
+ end
118
+ end
119
+
120
+ # {docs/behavior.md B-16}[link:../../../docs/behavior.md] — An Kobako::Handle arriving as a positional or keyword
121
+ # argument identifies a host-side object previously allocated by a prior
122
+ # transport call's Handle wrap (B-14). Resolve it back to the Ruby object before
123
+ # the dispatch reaches +public_send+.
124
+ def resolve_arg(value, handler)
125
+ case value
126
+ when Kobako::Handle
127
+ require_live_object!(value.id, handler)
128
+ else
129
+ value
130
+ end
131
+ end
132
+
133
+ # Resolve a Request target to the Ruby object the registry (or
134
+ # Catalog::Handles) holds. String targets go through the registry;
135
+ # Handle targets (ext 0x01) go through the Catalog::Handles.
136
+ #
137
+ # Target type is already validated by +Transport::Request.decode+
138
+ # before this method is reached, so no else-branch is needed here —
139
+ # the wire layer is the system boundary that enforces the invariant.
140
+ def resolve_target(target, namespaces, handler)
141
+ case target
142
+ when String
143
+ resolve_path(target, namespaces)
144
+ when Kobako::Handle
145
+ resolve_handle(target, handler)
146
+ end
147
+ end
148
+
149
+ def resolve_path(path, namespaces)
150
+ namespaces.lookup(path)
151
+ rescue KeyError => e
152
+ raise UndefinedTargetError, e.message
153
+ end
154
+
155
+ def resolve_handle(handle, handler)
156
+ require_live_object!(handle.id, handler)
157
+ end
158
+
159
+ # Resolve +id+ through the Catalog::Handles. An unknown id (E-13)
160
+ # surfaces as UndefinedTargetError.
161
+ def require_live_object!(id, handler)
162
+ handler.fetch(id)
163
+ rescue Kobako::SandboxError => e
164
+ raise UndefinedTargetError, e.message
165
+ end
166
+
167
+ # Encode +value+ as a +Response.ok+ envelope. When the value is not
168
+ # wire-representable per {docs/behavior.md B-13}[link:../../../docs/behavior.md]'s type
169
+ # mapping, the +UnsupportedType+ rescue routes it through the
170
+ # Catalog::Handles via {#wrap_as_handle} and re-encodes with the Capability
171
+ # Handle in place ({docs/behavior.md B-14}[link:../../../docs/behavior.md]). The happy
172
+ # path encodes exactly once.
173
+ def encode_ok(value, handler)
174
+ response = Kobako::Transport::Response.ok(value)
175
+ response.encode
176
+ rescue Kobako::Codec::UnsupportedType
177
+ encode_ok(wrap_as_handle(value, handler), handler)
178
+ end
179
+
180
+ # Allocate +value+ in the Sandbox's Catalog::Handles and return a +Handle+
181
+ # that the wire codec can carry ({docs/behavior.md B-14}[link:../../../docs/behavior.md]).
182
+ # Used as the fallback path of {#encode_ok} when +value+ has no wire
183
+ # representation.
184
+ def wrap_as_handle(value, handler)
185
+ handler.alloc(value)
186
+ end
187
+
188
+ def encode_error(type, message)
189
+ fault = Kobako::Fault.new(type: type, message: message)
190
+ response = Kobako::Transport::Response.error(fault)
191
+ response.encode
192
+ end
193
+ end
194
+ end
195
+ end
@@ -3,21 +3,22 @@
3
3
  require_relative "../errors"
4
4
 
5
5
  module Kobako
6
- module RPC
6
+ module Transport
7
7
  # +Kobako::SandboxError+ subclass raised when the host detects a
8
8
  # structural violation of the wire contract while decoding bytes
9
9
  # produced by the guest (a malformed Outcome envelope, a result body
10
10
  # that fails msgpack decode, a Panic envelope missing required
11
11
  # fields). Distinct from a Wasm trap (engine signalled the guest
12
12
  # runtime is unrecoverable) and from a normal sandbox-layer failure
13
- # (the script raised but the protocol was respected): a +WireError+
14
- # always indicates the guest runtime is corrupted — the only safe
15
- # recovery is to discard the Sandbox and start a new invocation.
13
+ # (the script raised but the protocol was respected): a
14
+ # +Transport::Error+ always indicates the guest runtime is corrupted —
15
+ # the only safe recovery is to discard the Sandbox and start a new
16
+ # invocation.
16
17
  #
17
18
  # Inherits from +Kobako::SandboxError+ so a single
18
19
  # +rescue Kobako::SandboxError+ still catches it; callers that want
19
20
  # to distinguish wire-violation paths from script failures can
20
- # +rescue Kobako::RPC::WireError+ directly.
21
- class WireError < Kobako::SandboxError; end
21
+ # +rescue Kobako::Transport::Error+ directly.
22
+ class Error < Kobako::SandboxError; end
22
23
  end
23
24
  end