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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +1 -1
- data/README.md +0 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +4 -2
- data/ext/kobako/src/{wasm → runtime}/cache.rs +12 -16
- data/ext/kobako/src/runtime/capture.rs +91 -0
- data/ext/kobako/src/runtime/config.rs +26 -0
- data/ext/kobako/src/runtime/dispatch.rs +211 -0
- data/ext/kobako/src/runtime/exports.rs +51 -0
- data/ext/kobako/src/runtime/guest_mem.rs +228 -0
- data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +94 -86
- data/ext/kobako/src/runtime/trap.rs +134 -0
- data/ext/kobako/src/runtime.rs +782 -0
- data/ext/kobako/src/snapshot.rs +110 -0
- data/lib/kobako/capture.rb +11 -16
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +13 -5
- data/lib/kobako/codec/factory.rb +12 -12
- data/lib/kobako/codec/utils.rb +56 -59
- data/lib/kobako/codec.rb +6 -3
- data/lib/kobako/errors.rb +45 -28
- data/lib/kobako/fault.rb +40 -0
- data/lib/kobako/handle.rb +4 -6
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +31 -35
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +83 -72
- data/lib/kobako/sandbox_options.rb +6 -9
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +6 -7
- data/lib/kobako/snippet/source.rb +8 -8
- data/lib/kobako/snippet.rb +7 -9
- data/lib/kobako/transport/dispatcher.rb +195 -0
- data/lib/kobako/{rpc/wire_error.rb → transport/error.rb} +7 -6
- data/lib/kobako/transport/request.rb +78 -0
- data/lib/kobako/transport/response.rb +69 -0
- data/lib/kobako/transport/run.rb +141 -0
- data/lib/kobako/transport/yield.rb +91 -0
- data/lib/kobako/transport/yielder.rb +89 -0
- data/lib/kobako/transport.rb +24 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +4 -4
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/catalog/handles.rbs +19 -0
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
- data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
- data/sig/kobako/codec/decoder.rbs +2 -1
- data/sig/kobako/codec/factory.rbs +2 -2
- data/sig/kobako/codec/utils.rbs +5 -5
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +2 -3
- data/sig/kobako/namespace.rbs +19 -0
- data/sig/kobako/outcome.rbs +2 -2
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +5 -8
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/transport/dispatcher.rbs +34 -0
- data/sig/kobako/transport/error.rbs +6 -0
- data/sig/kobako/transport/request.rbs +32 -0
- data/sig/kobako/transport/response.rbs +30 -0
- data/sig/kobako/transport/run.rbs +27 -0
- data/sig/kobako/transport/yield.rbs +34 -0
- data/sig/kobako/transport/yielder.rbs +21 -0
- data/sig/kobako/transport.rbs +4 -0
- metadata +48 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -162
- data/ext/kobako/src/wasm/instance.rs +0 -873
- data/ext/kobako/src/wasm.rs +0 -126
- data/lib/kobako/handle_table.rb +0 -119
- data/lib/kobako/invocation.rb +0 -143
- data/lib/kobako/rpc/dispatcher.rb +0 -171
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -146
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/handle_table.rbs +0 -23
- data/sig/kobako/invocation.rbs +0 -25
- data/sig/kobako/rpc/dispatcher.rbs +0 -33
- data/sig/kobako/rpc/envelope.rbs +0 -51
- data/sig/kobako/rpc/fault.rbs +0 -20
- data/sig/kobako/rpc/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -31
- data/sig/kobako/rpc/wire_error.rbs +0 -6
- data/sig/kobako/wasm.rbs +0 -41
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -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::
|
|
22
|
-
# +Kobako::
|
|
23
|
-
# the per-instance
|
|
24
|
-
# injection so guest→host dispatch and host→guest
|
|
25
|
-
# allocator), and the
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
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, :
|
|
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::
|
|
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
|
-
@
|
|
106
|
-
@services = Kobako::
|
|
107
|
-
@snippets =
|
|
108
|
-
@
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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
|
|
164
|
-
# +Kobako::
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
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
|
-
|
|
171
|
+
run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
172
172
|
invoke!(:run) do
|
|
173
|
-
@
|
|
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
|
-
@
|
|
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
|
-
#
|
|
215
|
-
# itself is held as +@
|
|
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
|
-
@
|
|
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
|
|
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 = @
|
|
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
|
-
#
|
|
270
|
-
#
|
|
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
|
-
#
|
|
273
|
-
#
|
|
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
|
|
277
|
-
when
|
|
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.
|
|
285
|
-
#
|
|
286
|
-
#
|
|
287
|
-
# +
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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::
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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::
|
|
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
|
-
#
|
|
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 +
|
|
21
|
-
# +Binary.new(body: ...)+. Wire-form construction is the
|
|
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::
|
|
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 +
|
|
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 +
|
|
17
|
-
# instances via keyword form +Source.new(name: ..., body: ...)+.
|
|
18
|
-
# Wire-form construction is the
|
|
19
|
-
#
|
|
20
|
-
#
|
|
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
|
data/lib/kobako/snippet.rb
CHANGED
|
@@ -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 —
|
|
9
|
-
#
|
|
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
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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
|
|
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
|
|
14
|
-
# always indicates the guest runtime is corrupted —
|
|
15
|
-
# recovery is to discard the Sandbox and start a new
|
|
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::
|
|
21
|
-
class
|
|
21
|
+
# +rescue Kobako::Transport::Error+ directly.
|
|
22
|
+
class Error < Kobako::SandboxError; end
|
|
22
23
|
end
|
|
23
24
|
end
|