kobako 0.4.0 → 0.6.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 +44 -0
- data/Cargo.lock +1 -1
- data/README.md +89 -205
- 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 +100 -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 +83 -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 +88 -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 +79 -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 +108 -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 +7 -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 +24 -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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Host-side wasmtime runtime, surfaced as a Ruby class by the native ext
|
|
5
|
+
# (see ext/kobako/src/runtime.rs). The +Kobako::Runtime+ class wraps the
|
|
6
|
+
# wasmtime engine + compiled module + Store; it is the only Ruby-visible
|
|
7
|
+
# wasmtime type and the foundational binding layer for +Kobako::Sandbox+.
|
|
8
|
+
#
|
|
9
|
+
# This file reopens the magnus-defined class only to add the pure-Ruby
|
|
10
|
+
# +.default_path+ helper. Every other method (+#from_path+ singleton,
|
|
11
|
+
# +#eval+ / +#run+, capture and usage readers) is registered from Rust
|
|
12
|
+
# at ext load time.
|
|
13
|
+
class Runtime
|
|
14
|
+
# Absolute path to the gem-bundled +data/kobako.wasm+ artifact. Computed
|
|
15
|
+
# from this file's location so it works for both +bundle exec+ (running
|
|
16
|
+
# from the repo) and an installed gem (running from the gem dir).
|
|
17
|
+
#
|
|
18
|
+
# Returns a String regardless of whether the file currently exists —
|
|
19
|
+
# call sites that need the file to be present should pass this through
|
|
20
|
+
# +Kobako::Runtime.from_path+, which raises
|
|
21
|
+
# +Kobako::ModuleNotBuiltError+ with a clear remediation message.
|
|
22
|
+
# Raises +Kobako::SetupError+ if +__dir__+ is unavailable so that
|
|
23
|
+
# +rescue Kobako::SetupError+ around +Kobako::Sandbox.new+ catches every
|
|
24
|
+
# construction-layer failure uniformly, including this path-resolution one.
|
|
25
|
+
def self.default_path
|
|
26
|
+
dir = __dir__ or raise Kobako::SetupError, "Kobako::Runtime.default_path requires __dir__"
|
|
27
|
+
File.expand_path("../../data/kobako.wasm", dir)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -3,29 +3,29 @@
|
|
|
3
3
|
require "forwardable"
|
|
4
4
|
|
|
5
5
|
require_relative "capture"
|
|
6
|
+
require_relative "codec"
|
|
6
7
|
require_relative "errors"
|
|
7
|
-
require_relative "handle_table"
|
|
8
|
-
require_relative "invocation"
|
|
9
8
|
require_relative "outcome"
|
|
10
|
-
require_relative "rpc/server"
|
|
11
|
-
require_relative "rpc/envelope"
|
|
12
9
|
require_relative "sandbox_options"
|
|
13
|
-
require_relative "snippet"
|
|
14
10
|
require_relative "usage"
|
|
11
|
+
require_relative "transport"
|
|
12
|
+
require_relative "catalog"
|
|
15
13
|
|
|
16
14
|
module Kobako
|
|
17
15
|
# Kobako::Sandbox — the user-facing entry point for executing guest mruby
|
|
18
16
|
# scripts inside a wasmtime-hosted Wasm module
|
|
19
17
|
# ({docs/behavior.md B-01}[link:../../docs/behavior.md]).
|
|
20
18
|
#
|
|
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
|
-
#
|
|
19
|
+
# The Sandbox owns the +Kobako::Runtime+, the per-Sandbox
|
|
20
|
+
# +Kobako::Catalog::Handles+ ({docs/behavior.md B-19}[link:../../docs/behavior.md]),
|
|
21
|
+
# the per-instance +Kobako::Catalog::Namespaces+ (which receives the
|
|
22
|
+
# +Catalog::Handles+ by injection so guest→host dispatch and host→guest
|
|
23
|
+
# auto-wrap share one allocator), and the dispatch +Proc+ /
|
|
24
|
+
# +yield_to_guest+ lambda installed on the Runtime via
|
|
25
|
+
# +Runtime#on_dispatch=+ ({docs/behavior.md B-12}[link:../../docs/behavior.md]).
|
|
26
|
+
# The underlying wasmtime Engine and compiled Module are cached at process
|
|
27
|
+
# scope by the native ext and never surface to Ruby — constructing many
|
|
28
|
+
# Sandboxes amortises both costs automatically.
|
|
29
29
|
#
|
|
30
30
|
# Output capture policy ({docs/behavior.md B-04}[link:../../docs/behavior.md]): the
|
|
31
31
|
# per-channel cap (+stdout_limit+ / +stderr_limit+) is enforced inside the
|
|
@@ -38,9 +38,7 @@ module Kobako
|
|
|
38
38
|
class Sandbox
|
|
39
39
|
extend Forwardable
|
|
40
40
|
|
|
41
|
-
attr_reader :wasm_path, :
|
|
42
|
-
:options,
|
|
43
|
-
:services, :snippets
|
|
41
|
+
attr_reader :wasm_path, :options
|
|
44
42
|
|
|
45
43
|
# Per-cap accessors forward to the immutable +SandboxOptions+ Value
|
|
46
44
|
# Object so the Host App still reads them off Sandbox directly.
|
|
@@ -99,21 +97,22 @@ module Kobako
|
|
|
99
97
|
def initialize(wasm_path: nil, stdout_limit: nil, stderr_limit: nil,
|
|
100
98
|
timeout: SandboxOptions::DEFAULT_TIMEOUT_SECONDS,
|
|
101
99
|
memory_limit: SandboxOptions::DEFAULT_MEMORY_LIMIT)
|
|
102
|
-
@wasm_path = wasm_path || Kobako::
|
|
100
|
+
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
103
101
|
@options = SandboxOptions.new(timeout: timeout, memory_limit: memory_limit, stdout_limit: stdout_limit,
|
|
104
102
|
stderr_limit: stderr_limit)
|
|
105
|
-
@
|
|
106
|
-
@services = Kobako::
|
|
107
|
-
@snippets =
|
|
108
|
-
@
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
@handler = Catalog::Handles.new
|
|
104
|
+
@services = Kobako::Catalog::Namespaces.new(handler: @handler)
|
|
105
|
+
@snippets = Catalog::Snippets.new
|
|
106
|
+
@runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
107
|
+
@options.stdout_limit, @options.stderr_limit)
|
|
108
|
+
install_dispatch_proc!
|
|
111
109
|
reset_invocation_state!
|
|
112
110
|
end
|
|
113
111
|
|
|
114
112
|
# Declare or retrieve the Namespace named +name+ on this Sandbox
|
|
115
113
|
# ({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
|
|
114
|
+
# Symbol or String in constant form. Returns the
|
|
115
|
+
# +Kobako::Namespace+.
|
|
117
116
|
#
|
|
118
117
|
# Raises +ArgumentError+ when called after the first invocation, or
|
|
119
118
|
# when +name+ does not match the constant-name pattern.
|
|
@@ -160,17 +159,19 @@ module Kobako
|
|
|
160
159
|
|
|
161
160
|
# Dispatch into a preloaded entrypoint constant
|
|
162
161
|
# ({docs/behavior.md B-31}[link:../../docs/behavior.md]). Delegates host
|
|
163
|
-
# pre-flight
|
|
164
|
-
# +Kobako::
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
162
|
+
# pre-flight and wire encoding to +Kobako::Transport::Run+ /
|
|
163
|
+
# +Kobako::Transport::Run#encode+: a non-Symbol/String +target+ raises
|
|
164
|
+
# +TypeError+ (E-24), while a +target+ failing the constant pattern
|
|
165
|
+
# (E-25), a forged +Kobako::Handle+ in +args+ / +kwargs+ (E-29), or a
|
|
166
|
+
# non-Symbol +kwargs+ key (E-30) raise +ArgumentError+. The guest
|
|
167
|
+
# resolves +target+ as a top-level constant, calls +#call+ on it with
|
|
168
|
+
# +args+ / +kwargs+, and returns the deserialized result. The first
|
|
169
|
+
# invocation seals the Service registry and snippet table (B-07 /
|
|
170
|
+
# B-33). Runtime errors follow the same three-class taxonomy as +#eval+.
|
|
170
171
|
def run(target, *args, **kwargs)
|
|
171
|
-
|
|
172
|
+
run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
172
173
|
invoke!(:run) do
|
|
173
|
-
@
|
|
174
|
+
@runtime.run(@services.encode, @snippets.encode, run_envelope.encode(@handler))
|
|
174
175
|
end
|
|
175
176
|
end
|
|
176
177
|
|
|
@@ -201,23 +202,38 @@ module Kobako
|
|
|
201
202
|
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
202
203
|
|
|
203
204
|
invoke!(:eval) do
|
|
204
|
-
@
|
|
205
|
+
@runtime.eval(@services.encode, code.b, @snippets.encode)
|
|
205
206
|
end
|
|
206
207
|
end
|
|
207
208
|
|
|
208
209
|
private
|
|
209
210
|
|
|
211
|
+
# Configure the +Runtime+'s host↔guest dispatch wiring
|
|
212
|
+
# ({docs/behavior.md B-12}[link:../../docs/behavior.md]). Builds a
|
|
213
|
+
# lambda that re-enters the guest via
|
|
214
|
+
# +Runtime#yield_to_active_invocation+ (B-24) and a dispatch +Proc+
|
|
215
|
+
# that routes guest→host calls through the stateless
|
|
216
|
+
# +Transport::Dispatcher+, capturing +@services+ / +@handler+ in the
|
|
217
|
+
# closure. Both are registered on the +Runtime+ once at construction
|
|
218
|
+
# time so the wasm ext callback can fire without further setup.
|
|
219
|
+
def install_dispatch_proc!
|
|
220
|
+
yield_to_guest = ->(args_bytes) { @runtime.yield_to_active_invocation(args_bytes) }
|
|
221
|
+
@runtime.on_dispatch = lambda do |request_bytes|
|
|
222
|
+
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, yield_to_guest)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
210
226
|
# Per-invocation prologue ({docs/behavior.md B-03 / B-07 /
|
|
211
227
|
# B-33}[link:../../docs/behavior.md]). Seals the Service / snippet
|
|
212
228
|
# registries on first call (idempotent) and zeros the per-invocation
|
|
213
229
|
# capability state — capture buffers, truncation predicates, and the
|
|
214
|
-
#
|
|
215
|
-
# itself is held as +@
|
|
230
|
+
# +Catalog::Handles+ counter — before the guest runs. The
|
|
231
|
+
# +Catalog::Handles+ itself is held as +@handler+ and never exposed beyond
|
|
216
232
|
# this class: SPEC.md Terminology pins it as "Not exposed to the
|
|
217
233
|
# Host App" (B-19 / B-20 / E-29).
|
|
218
234
|
def begin_invocation!
|
|
219
235
|
@services.seal!
|
|
220
|
-
@
|
|
236
|
+
@handler.reset!
|
|
221
237
|
reset_invocation_state!
|
|
222
238
|
end
|
|
223
239
|
|
|
@@ -235,66 +251,66 @@ module Kobako
|
|
|
235
251
|
@usage = Usage::EMPTY
|
|
236
252
|
end
|
|
237
253
|
|
|
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
254
|
# Read the per-last-invocation +wall_time+ and +memory_peak+ from
|
|
252
255
|
# the ext and wrap them as a +Kobako::Usage+ value object
|
|
253
256
|
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
|
|
254
257
|
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
255
258
|
# every outcome — value return, +Kobako::TrapError+ (including
|
|
256
259
|
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
257
|
-
# and +Kobako::ServiceError+.
|
|
260
|
+
# and +Kobako::ServiceError+. On the success path the same figures
|
|
261
|
+
# already arrive via +Snapshot#usage+; on the trap path the Snapshot
|
|
262
|
+
# never reaches Ruby so the ext readout here is the only source.
|
|
258
263
|
#
|
|
259
264
|
# The ext returns a positional 2-tuple +[wall_time, memory_peak]+
|
|
260
265
|
# whose order matches the +Kobako::Usage+ field order; the
|
|
261
266
|
# destructure-then-kwargs handoff below is the explicit
|
|
262
|
-
# positional→keyword conversion point
|
|
263
|
-
# +#read_captures!+'s +Capture.from_ext(bytes, truncated)+ shape.
|
|
267
|
+
# positional→keyword conversion point.
|
|
264
268
|
def read_usage!
|
|
265
|
-
wall_time, memory_peak = @
|
|
269
|
+
wall_time, memory_peak = @runtime.usage
|
|
266
270
|
@usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
267
271
|
end
|
|
268
272
|
|
|
269
|
-
#
|
|
270
|
-
#
|
|
273
|
+
# Pick the +TrapError+ subclass to re-raise based on +err+'s actual
|
|
274
|
+
# class. Cap-trap subclasses
|
|
271
275
|
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
|
|
272
|
-
#
|
|
273
|
-
#
|
|
276
|
+
# preserve their named identity; everything else collapses to the
|
|
277
|
+
# base +Kobako::TrapError+. The ext already raises the right subclass
|
|
278
|
+
# directly, so this is a pure re-attribution that lets +#invoke!+
|
|
279
|
+
# add the verb prefix without erasing +TimeoutError+ /
|
|
280
|
+
# +MemoryLimitError+.
|
|
274
281
|
def trap_class_for(err)
|
|
275
282
|
case err
|
|
276
|
-
when
|
|
277
|
-
when
|
|
283
|
+
when TimeoutError then TimeoutError
|
|
284
|
+
when MemoryLimitError then MemoryLimitError
|
|
278
285
|
else TrapError
|
|
279
286
|
end
|
|
280
287
|
end
|
|
281
288
|
|
|
282
289
|
# Shared prologue / epilogue + trap-class translator for both
|
|
283
290
|
# 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
|
-
#
|
|
291
|
+
# TrapError message so the failing export is identifiable.
|
|
292
|
+
#
|
|
293
|
+
# The yielded block must return a +Kobako::Snapshot+ — i.e. the
|
|
294
|
+
# value of +Runtime#eval+ / +#run+ (SPEC.md Internal Concepts →
|
|
295
|
+
# Snapshot). The success path unpacks every observable from the
|
|
296
|
+
# Snapshot in one go: +#stdout+ / +#stderr+ pack into +Capture+,
|
|
297
|
+
# +#usage+ packs into +Usage+, +#return_bytes+ feeds +Outcome.decode+.
|
|
298
|
+
# The rescue chain is the single trap-translation boundary —
|
|
299
|
+
# configured-cap paths
|
|
300
|
+
# ({docs/behavior.md E-19 / E-20}[link:../../docs/behavior.md])
|
|
301
|
+
# surface as named TrapError subclasses; everything else surfaces as
|
|
302
|
+
# the base +TrapError+.
|
|
292
303
|
def invoke!(verb)
|
|
293
304
|
begin_invocation!
|
|
294
|
-
yield
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
snapshot = yield
|
|
306
|
+
@stdout_capture = snapshot.stdout
|
|
307
|
+
@stderr_capture = snapshot.stderr
|
|
308
|
+
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
309
|
+
# token; restore it to the host object the guest referenced before
|
|
310
|
+
# handing the value to the Host App (B-37). @handler still holds this
|
|
311
|
+
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
312
|
+
Codec::Utils.deep_restore(Outcome.decode(snapshot.return_bytes), @handler)
|
|
313
|
+
rescue Kobako::TrapError => e
|
|
298
314
|
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
299
315
|
ensure
|
|
300
316
|
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, handler) 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
|