kobako 0.18.0 → 0.20.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 -1
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +6 -5
- data/README.md +4 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +6 -2
- data/ext/kobako/src/runtime/bridge.rs +30 -24
- data/ext/kobako/src/runtime/gvl.rs +114 -0
- data/ext/kobako/src/runtime.rs +211 -190
- data/lib/kobako/catalog/extensions.rb +50 -43
- data/lib/kobako/catalog/handles.rb +7 -16
- data/lib/kobako/catalog/services.rb +22 -36
- data/lib/kobako/catalog/snippets.rb +11 -10
- data/lib/kobako/context.rb +202 -0
- data/lib/kobako/errors.rb +26 -4
- data/lib/kobako/execution.rb +52 -0
- data/lib/kobako/extension.rb +24 -10
- data/lib/kobako/pool.rb +1 -7
- data/lib/kobako/sandbox.rb +43 -182
- data/lib/kobako/sandbox_options.rb +23 -2
- data/lib/kobako/transport/dispatcher.rb +16 -15
- data/lib/kobako/transport/yielder.rb +1 -1
- data/lib/kobako/unresolved.rb +17 -0
- data/lib/kobako/usage.rb +6 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +2 -1
- data/sig/kobako/catalog/extensions.rbs +4 -6
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/catalog/services.rbs +3 -3
- data/sig/kobako/catalog/snippets.rbs +2 -0
- data/sig/kobako/context.rbs +36 -0
- data/sig/kobako/errors.rbs +12 -3
- data/sig/kobako/execution.rbs +24 -0
- data/sig/kobako/extension.rbs +4 -2
- data/sig/kobako/runtime.rbs +37 -9
- data/sig/kobako/sandbox.rbs +5 -24
- data/sig/kobako/sandbox_options.rbs +9 -2
- data/sig/kobako/transport/dispatcher.rbs +3 -3
- data/sig/kobako/unresolved.rbs +4 -0
- metadata +8 -1
data/lib/kobako/extension.rb
CHANGED
|
@@ -27,17 +27,31 @@ module Kobako
|
|
|
27
27
|
# Kobako::Extension::Backend — the host attachment of an Extension,
|
|
28
28
|
# pairing +path+ (the constant path the backend binds at, single-segment
|
|
29
29
|
# +"File"+ or nested +"MyApp::Store"+, spelling the guest constant the
|
|
30
|
-
# idiom routes to) with
|
|
30
|
+
# idiom routes to) with the source of the bound object, declared by
|
|
31
|
+
# explicit keyword:
|
|
31
32
|
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
33
|
+
# * +object:+ — a static object, fixed for the Sandbox's life.
|
|
34
|
+
# * +provider:+ — a no-argument callable invoked once per invocation to
|
|
35
|
+
# yield that invocation's object, so a fresh object backs the path
|
|
36
|
+
# every invocation. A provider that raises propagates its exception to
|
|
37
|
+
# the invocation caller and leaves the guest unrun; the next
|
|
38
|
+
# invocation resolves it afresh.
|
|
39
|
+
# * neither — a fillable, defaulting to +Kobako::Unresolved+ until the
|
|
40
|
+
# host supplies the invocation's object.
|
|
41
|
+
#
|
|
42
|
+
# The kind is chosen by keyword, never inferred from whether the value is
|
|
43
|
+
# callable, so a static object that is itself callable is expressed
|
|
44
|
+
# directly with +object:+. Giving both +object:+ and +provider:+ raises
|
|
45
|
+
# +ArgumentError+.
|
|
46
|
+
class Backend < Data.define(:path, :object, :provider)
|
|
47
|
+
def initialize(path:, object: nil, provider: nil)
|
|
48
|
+
if !object.nil? && !provider.nil?
|
|
49
|
+
raise ArgumentError,
|
|
50
|
+
"Extension::Backend accepts object: or provider:, not both"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
super
|
|
54
|
+
end
|
|
41
55
|
end
|
|
42
56
|
|
|
43
57
|
# +backend+ and +depends_on+ default to absent so the common
|
data/lib/kobako/pool.rb
CHANGED
|
@@ -47,7 +47,7 @@ module Kobako
|
|
|
47
47
|
# discarded and its slot refills by a fresh construction on next
|
|
48
48
|
# demand.
|
|
49
49
|
def with
|
|
50
|
-
sandbox =
|
|
50
|
+
sandbox = acquire
|
|
51
51
|
begin
|
|
52
52
|
yield sandbox
|
|
53
53
|
rescue TrapError
|
|
@@ -61,12 +61,6 @@ module Kobako
|
|
|
61
61
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
|
-
# Acquire a Sandbox and hand it over in pre-invocation state — empty
|
|
65
|
-
# output buffers and truncation predicates false.
|
|
66
|
-
def checkout
|
|
67
|
-
acquire.tap(&:reset_invocation_state!)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
64
|
# The idle-first claim loop: an idle Sandbox wins, unclaimed
|
|
71
65
|
# capacity constructs, and a full pool waits for a checkin.
|
|
72
66
|
def acquire
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -2,37 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
require "forwardable"
|
|
4
4
|
|
|
5
|
-
require_relative "capture"
|
|
6
|
-
require_relative "codec"
|
|
7
5
|
require_relative "errors"
|
|
8
|
-
require_relative "
|
|
6
|
+
require_relative "unresolved"
|
|
9
7
|
require_relative "sandbox_options"
|
|
10
|
-
require_relative "usage"
|
|
11
8
|
require_relative "transport"
|
|
12
9
|
require_relative "catalog"
|
|
10
|
+
require_relative "context"
|
|
13
11
|
|
|
14
12
|
module Kobako
|
|
15
13
|
# Kobako::Sandbox — the user-facing entry point for executing guest mruby
|
|
16
14
|
# scripts inside a wasmtime-hosted Wasm module.
|
|
17
15
|
#
|
|
18
|
-
# The Sandbox owns the +Kobako::Runtime
|
|
19
|
-
# +Kobako::Catalog::
|
|
20
|
-
# +
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
16
|
+
# The Sandbox owns the reusable configuration — the +Kobako::Runtime+ and
|
|
17
|
+
# the +Kobako::Catalog::Services+ / +Catalog::Snippets+ / +Catalog::Extensions+
|
|
18
|
+
# registries. Each +#eval+ / +#run+ seals that config on the first call and
|
|
19
|
+
# drives one guest invocation through a fresh +Kobako::Context+ — its own
|
|
20
|
+
# Handle table, dispatch +Proc+, captures, and usage — so the reusable
|
|
21
|
+
# Sandbox holds no per-invocation state. The underlying wasmtime Engine and
|
|
22
|
+
# compiled Module are cached at process scope by the native ext and never
|
|
23
|
+
# surface to Ruby — constructing many Sandboxes amortises both costs
|
|
26
24
|
# automatically.
|
|
27
25
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# +#stdout_truncated?+ / +#stderr_truncated?+ are the only way to observe
|
|
35
|
-
# that the cap was hit.
|
|
26
|
+
# A run's observables — the captured +#stdout+ / +#stderr+ (bounded by
|
|
27
|
+
# +stdout_limit+ / +stderr_limit+, enforced inside the WASI pipe) with their
|
|
28
|
+
# +#stdout_truncated?+ / +#stderr_truncated?+ predicates, and +#usage+ — live
|
|
29
|
+
# on the +Kobako::Execution+ each +#eval+ / +#run+ returns, or on the one its
|
|
30
|
+
# raised error carries. The Sandbox itself keeps none of them, so nothing a
|
|
31
|
+
# run observes carries into the next one.
|
|
36
32
|
class Sandbox
|
|
37
33
|
extend Forwardable
|
|
38
34
|
|
|
@@ -40,39 +36,7 @@ module Kobako
|
|
|
40
36
|
|
|
41
37
|
# Per-option accessors forward to the immutable +SandboxOptions+ Value
|
|
42
38
|
# Object so the Host App still reads them off Sandbox directly.
|
|
43
|
-
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile
|
|
44
|
-
|
|
45
|
-
# Returns the bytes the guest wrote to stdout during the most recent
|
|
46
|
-
# invocation as a UTF-8 String, clipped at +stdout_limit+. Empty before
|
|
47
|
-
# any invocation; the byte content never contains a truncation sentinel,
|
|
48
|
-
# so use +#stdout_truncated?+ to observe overflow. Populated on every
|
|
49
|
-
# outcome — including a rescued +TrapError+, after which it holds the
|
|
50
|
-
# bytes written before the trap fired — mirroring +#usage+.
|
|
51
|
-
def stdout = @stdout_capture.bytes
|
|
52
|
-
|
|
53
|
-
# Returns the bytes the guest wrote to stderr during the most recent
|
|
54
|
-
# invocation as a UTF-8 String, clipped at +stderr_limit+. Empty before
|
|
55
|
-
# any invocation. Mirror of +#stdout+.
|
|
56
|
-
def stderr = @stderr_capture.bytes
|
|
57
|
-
|
|
58
|
-
# Returns +true+ iff stdout capture during the most recent invocation
|
|
59
|
-
# exceeded +stdout_limit+. Resets to +false+ at the start of the next
|
|
60
|
-
# invocation.
|
|
61
|
-
def stdout_truncated? = @stdout_capture.truncated?
|
|
62
|
-
|
|
63
|
-
# Returns +true+ iff stderr capture during the most recent invocation
|
|
64
|
-
# exceeded +stderr_limit+. Mirror of +#stdout_truncated?+.
|
|
65
|
-
def stderr_truncated? = @stderr_capture.truncated?
|
|
66
|
-
|
|
67
|
-
# Returns the +Kobako::Usage+ value object for the most recent
|
|
68
|
-
# invocation. Carries +wall_time+ (Float seconds the guest export call spent
|
|
69
|
-
# inside wasmtime) and +memory_peak+ (Integer bytes, high-water of
|
|
70
|
-
# the per-invocation +memory.grow+ delta past the entry-time
|
|
71
|
-
# baseline). Returns +Kobako::Usage::EMPTY+ before any invocation;
|
|
72
|
-
# populated on every outcome — including +TrapError+ — so the Host
|
|
73
|
-
# App can read it after rescuing a trap to diagnose budget
|
|
74
|
-
# consumption.
|
|
75
|
-
attr_reader :usage
|
|
39
|
+
def_delegators :@options, :timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile, :gvl
|
|
76
40
|
|
|
77
41
|
# Build a fresh Sandbox.
|
|
78
42
|
#
|
|
@@ -91,13 +55,10 @@ module Kobako
|
|
|
91
55
|
def initialize(wasm_path: nil, **)
|
|
92
56
|
@wasm_path = wasm_path || Kobako::Runtime.default_path
|
|
93
57
|
@options = SandboxOptions.new(**)
|
|
94
|
-
@
|
|
95
|
-
@services = Kobako::Catalog::Services.new(handler: @handler)
|
|
58
|
+
@services = Kobako::Catalog::Services.new
|
|
96
59
|
@snippets = Catalog::Snippets.new
|
|
97
60
|
@extensions = Catalog::Extensions.new
|
|
98
61
|
@runtime = build_runtime!
|
|
99
|
-
install_dispatch_proc!
|
|
100
|
-
reset_invocation_state!
|
|
101
62
|
end
|
|
102
63
|
|
|
103
64
|
# Bind +object+ as the Service reachable at +path+ — a Symbol or
|
|
@@ -105,11 +66,17 @@ module Kobako
|
|
|
105
66
|
# (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
|
|
106
67
|
# chaining.
|
|
107
68
|
#
|
|
69
|
+
# Called with only a +path+, it declares a fillable Service:
|
|
70
|
+
# +bind(path)+ reserves the path for +Kobako::Unresolved+, so the guest
|
|
71
|
+
# sees the constant while the host defers the object it stands for. A
|
|
72
|
+
# guest dispatch to an unfilled fillable surfaces as
|
|
73
|
+
# +Kobako::ServiceError+ when left unrescued.
|
|
74
|
+
#
|
|
108
75
|
# Raises +ArgumentError+ when a segment is malformed, when +path+
|
|
109
76
|
# collides with an existing binding (a name is a bound Service or a
|
|
110
77
|
# grouping prefix, never both), or when called after the first
|
|
111
78
|
# invocation has sealed Service registration.
|
|
112
|
-
def bind(path, object)
|
|
79
|
+
def bind(path, object = Unresolved)
|
|
113
80
|
@services.bind(path, object)
|
|
114
81
|
self
|
|
115
82
|
end
|
|
@@ -175,11 +142,9 @@ module Kobako
|
|
|
175
142
|
# deserialized result. The first invocation seals the Service registry
|
|
176
143
|
# and snippet table. Runtime errors follow the same three-class
|
|
177
144
|
# taxonomy as +#eval+.
|
|
178
|
-
def run(target, *args, **kwargs)
|
|
145
|
+
def run(target, *args, **kwargs, &block)
|
|
179
146
|
run_envelope = Transport::Run.new(entrypoint: target, args: args, kwargs: kwargs)
|
|
180
|
-
|
|
181
|
-
@runtime.run(@services.encode, @snippets.encode, run_envelope.encode(@handler))
|
|
182
|
-
end
|
|
147
|
+
new_invocation.run(run_envelope, &block)
|
|
183
148
|
end
|
|
184
149
|
|
|
185
150
|
# Execute a guest mruby source string in a fresh +mrb_state+. +code+ is
|
|
@@ -203,24 +168,10 @@ module Kobako
|
|
|
203
168
|
# (including when +code+ is +nil+ or not a String, or when a preloaded
|
|
204
169
|
# snippet's replay raises); +Kobako::ServiceError+ on an unrescued
|
|
205
170
|
# Service capability failure.
|
|
206
|
-
def eval(code)
|
|
171
|
+
def eval(code, &block)
|
|
207
172
|
raise SandboxError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
208
173
|
|
|
209
|
-
|
|
210
|
-
@runtime.eval(@services.encode, code.b, @snippets.encode)
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# Reset all per-invocation observable state to its pre-invocation
|
|
215
|
-
# sentinels — both per-channel captures and the per-last-invocation
|
|
216
|
-
# usage record. Shared by +#initialize+ (first-time setup) and
|
|
217
|
-
# +#begin_invocation!+ (between-invocation reset) so both paths agree on
|
|
218
|
-
# what "pre-invocation state" means; +Kobako::Pool+ calls it at checkout
|
|
219
|
-
# so a pooled Sandbox hands over empty output buffers.
|
|
220
|
-
def reset_invocation_state!
|
|
221
|
-
@stdout_capture = Capture::EMPTY
|
|
222
|
-
@stderr_capture = Capture::EMPTY
|
|
223
|
-
@usage = Usage::EMPTY
|
|
174
|
+
new_invocation.eval(code, &block)
|
|
224
175
|
end
|
|
225
176
|
|
|
226
177
|
private
|
|
@@ -231,119 +182,29 @@ module Kobako
|
|
|
231
182
|
# runtime that cannot honor the request never runs guest code.
|
|
232
183
|
def build_runtime!
|
|
233
184
|
runtime = Kobako::Runtime.from_path(@wasm_path, @options.timeout, @options.memory_limit,
|
|
234
|
-
@options.stdout_limit, @options.stderr_limit, @options.profile
|
|
185
|
+
@options.stdout_limit, @options.stderr_limit, @options.profile,
|
|
186
|
+
@options.gvl)
|
|
235
187
|
@options.enforce_floor!(runtime.profile)
|
|
236
188
|
runtime
|
|
237
189
|
end
|
|
238
190
|
|
|
239
|
-
#
|
|
240
|
-
#
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
# wasm ext callback can fire without further setup.
|
|
247
|
-
def install_dispatch_proc!
|
|
248
|
-
@runtime.on_dispatch = lambda do |request_bytes, guest_yielder|
|
|
249
|
-
Transport::Dispatcher.dispatch(request_bytes, @services, @handler, guest_yielder)
|
|
250
|
-
end
|
|
191
|
+
# Seal the config on the first invocation and return a fresh
|
|
192
|
+
# per-invocation +Context+ for the verb to drive. The Context owns this
|
|
193
|
+
# run's Handle table, resolved Extension backends, captures, and usage, so
|
|
194
|
+
# no per-invocation state is written back onto the shared config.
|
|
195
|
+
def new_invocation
|
|
196
|
+
begin_invocation!
|
|
197
|
+
Context.new(runtime: @runtime, services: @services, snippets: @snippets, extensions: @extensions)
|
|
251
198
|
end
|
|
252
199
|
|
|
253
|
-
# Per-invocation prologue
|
|
254
|
-
# registries on first call (idempotent — asserting Extension
|
|
255
|
-
# dependencies then)
|
|
256
|
-
#
|
|
257
|
-
# state — capture buffers, truncation predicates, and the
|
|
258
|
-
# +Catalog::Handles+ counter — before the guest runs. The
|
|
259
|
-
# +Catalog::Handles+ itself is held as +@handler+ and never exposed
|
|
260
|
-
# beyond this class — it is not part of the Host App's surface.
|
|
200
|
+
# Per-invocation prologue on the config tier: seals the Service / snippet /
|
|
201
|
+
# Extension registries on the first call (idempotent — asserting Extension
|
|
202
|
+
# dependencies then). Per-invocation provider resolution and observable
|
|
203
|
+
# state live on the +Context+, not here.
|
|
261
204
|
def begin_invocation!
|
|
262
205
|
@services.seal!
|
|
206
|
+
@snippets.seal!
|
|
263
207
|
@extensions.seal!
|
|
264
|
-
@extensions.refresh_backends!(@services)
|
|
265
|
-
@handler.reset!
|
|
266
|
-
reset_invocation_state!
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
# Read the per-last-invocation +wall_time+ and +memory_peak+ from
|
|
270
|
-
# the ext and wrap them as a +Kobako::Usage+ value object. Runs in
|
|
271
|
-
# the +invoke!+ +ensure+ block so the usage record is populated on
|
|
272
|
-
# every outcome — value return, +Kobako::TrapError+ (including
|
|
273
|
-
# +TimeoutError+ / +MemoryLimitError+), +Kobako::SandboxError+,
|
|
274
|
-
# and +Kobako::ServiceError+. +Runtime#usage+ is the single source for
|
|
275
|
-
# both paths: the figures are stashed in the ext on every outcome, so
|
|
276
|
-
# the readout here also covers the trap path, where +Runtime#eval+ /
|
|
277
|
-
# +#run+ raise instead of returning outcome bytes.
|
|
278
|
-
#
|
|
279
|
-
# The ext-side contract is positional: +Runtime#usage+ yields
|
|
280
|
-
# +[wall_time, memory_peak]+ in +Kobako::Usage+ field order.
|
|
281
|
-
def read_usage!
|
|
282
|
-
wall_time, memory_peak = @runtime.usage
|
|
283
|
-
@usage = Usage.new(wall_time: wall_time, memory_peak: memory_peak)
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# Pick the +TrapError+ subclass to re-raise based on +err+'s actual
|
|
287
|
-
# class. Cap-trap subclasses (+TimeoutError+ / +MemoryLimitError+)
|
|
288
|
-
# preserve their named identity; everything else collapses to the
|
|
289
|
-
# base +Kobako::TrapError+. The ext already raises the right subclass
|
|
290
|
-
# directly, so this is a pure re-attribution that lets +#invoke!+
|
|
291
|
-
# add the verb prefix without erasing +TimeoutError+ /
|
|
292
|
-
# +MemoryLimitError+.
|
|
293
|
-
def trap_class_for(err)
|
|
294
|
-
case err
|
|
295
|
-
when TimeoutError then TimeoutError
|
|
296
|
-
when MemoryLimitError then MemoryLimitError
|
|
297
|
-
else TrapError
|
|
298
|
-
end
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
# Read the per-last-invocation output captures from the ext and wrap
|
|
302
|
-
# them as +Kobako::Capture+ value objects. Runs in the +invoke!+
|
|
303
|
-
# +ensure+ block next to #read_usage! for the same reason: the ext
|
|
304
|
-
# stashes the captures on every outcome, so the readout also covers
|
|
305
|
-
# the trap path, where +Runtime#eval+ / +#run+ raise instead of
|
|
306
|
-
# returning outcome bytes — +#stdout+ / +#stderr+ keep the guest's
|
|
307
|
-
# partial output readable after a rescue.
|
|
308
|
-
#
|
|
309
|
-
# The ext-side contract is positional: +Runtime#captures+ yields
|
|
310
|
-
# +[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]+.
|
|
311
|
-
def read_captures!
|
|
312
|
-
stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated = @runtime.captures
|
|
313
|
-
@stdout_capture = Capture.new(bytes: stdout_bytes, truncated: stdout_truncated)
|
|
314
|
-
@stderr_capture = Capture.new(bytes: stderr_bytes, truncated: stderr_truncated)
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# Shared prologue / epilogue + trap-class translator for both
|
|
318
|
-
# invocation verbs. +verb+ is +:eval+ or +:run+; it tags the
|
|
319
|
-
# TrapError message so the failing export is identifiable.
|
|
320
|
-
#
|
|
321
|
-
# The yielded block must return the invocation's raw outcome bytes —
|
|
322
|
-
# i.e. the value of +Runtime#eval+ / +#run+ — which the success path
|
|
323
|
-
# feeds to +Outcome.decode+. Captures and usage are populated by the
|
|
324
|
-
# +ensure+ readouts (#read_usage! / #read_captures!) on every
|
|
325
|
-
# outcome, so +#stdout+ / +#stderr+ / +#usage+ stay readable after a
|
|
326
|
-
# rescued trap.
|
|
327
|
-
# The rescue chain is the single trap-translation boundary —
|
|
328
|
-
# configured-cap paths surface as named TrapError subclasses
|
|
329
|
-
# (+TimeoutError+ / +MemoryLimitError+); everything else surfaces as
|
|
330
|
-
# the base +TrapError+.
|
|
331
|
-
def invoke!(verb)
|
|
332
|
-
begin_invocation!
|
|
333
|
-
return_bytes = yield
|
|
334
|
-
# A Capability Handle in the result is decoded as a Kobako::Handle
|
|
335
|
-
# token; restore it to the host object the guest referenced before
|
|
336
|
-
# handing the value to the Host App. @handler still holds this
|
|
337
|
-
# invocation's table — reset only happens at the next #begin_invocation!.
|
|
338
|
-
# A Handle-free result resolves to itself, so the restoration walk is
|
|
339
|
-
# skipped when the decode carried none.
|
|
340
|
-
value, carried_handle = Codec.track_handles { Outcome.decode(return_bytes) }
|
|
341
|
-
carried_handle ? Codec::HandleWalk.deep_restore(value, @handler) : value
|
|
342
|
-
rescue Kobako::TrapError => e
|
|
343
|
-
raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
|
|
344
|
-
ensure
|
|
345
|
-
read_usage!
|
|
346
|
-
read_captures!
|
|
347
208
|
end
|
|
348
209
|
end
|
|
349
210
|
end
|
|
@@ -21,7 +21,7 @@ module Kobako
|
|
|
21
21
|
# as-is. The options also own the ladder comparison
|
|
22
22
|
# (+#enforce_floor!+) that +Kobako::Sandbox+ delegates its
|
|
23
23
|
# construction floor check to.
|
|
24
|
-
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile)
|
|
24
|
+
class SandboxOptions < Data.define(:timeout, :memory_limit, :stdout_limit, :stderr_limit, :profile, :gvl)
|
|
25
25
|
# Default wall-clock timeout for a single invocation: 60 seconds.
|
|
26
26
|
DEFAULT_TIMEOUT_SECONDS = 60.0
|
|
27
27
|
|
|
@@ -41,16 +41,28 @@ module Kobako
|
|
|
41
41
|
# +:permissive+ is the Host App's explicit trade.
|
|
42
42
|
DEFAULT_PROFILE = :hermetic
|
|
43
43
|
|
|
44
|
+
# The GVL scheduling modes: +:hold+ keeps Ruby's GVL for the whole
|
|
45
|
+
# invocation, +:release+ drops it for the guest span so distinct
|
|
46
|
+
# Sandboxes on distinct Threads run their guest code in parallel.
|
|
47
|
+
GVL_MODES = %i[hold release].freeze
|
|
48
|
+
|
|
49
|
+
# Default GVL mode: +:hold+ — holding the GVL matches single-threaded
|
|
50
|
+
# execution, so +:release+ is the Host App's explicit opt-in for
|
|
51
|
+
# host-parallel guest execution.
|
|
52
|
+
DEFAULT_GVL = :hold
|
|
53
|
+
|
|
44
54
|
def initialize(timeout: DEFAULT_TIMEOUT_SECONDS,
|
|
45
55
|
memory_limit: DEFAULT_MEMORY_LIMIT,
|
|
46
56
|
stdout_limit: DEFAULT_OUTPUT_LIMIT,
|
|
47
57
|
stderr_limit: DEFAULT_OUTPUT_LIMIT,
|
|
48
|
-
profile: DEFAULT_PROFILE
|
|
58
|
+
profile: DEFAULT_PROFILE,
|
|
59
|
+
gvl: DEFAULT_GVL)
|
|
49
60
|
timeout = normalize_timeout(timeout)
|
|
50
61
|
memory_limit = normalize_memory_limit(memory_limit)
|
|
51
62
|
stdout_limit = normalize_output_limit(stdout_limit, "stdout_limit")
|
|
52
63
|
stderr_limit = normalize_output_limit(stderr_limit, "stderr_limit")
|
|
53
64
|
profile = normalize_profile(profile)
|
|
65
|
+
gvl = normalize_gvl(gvl)
|
|
54
66
|
super
|
|
55
67
|
end
|
|
56
68
|
|
|
@@ -120,5 +132,14 @@ module Kobako
|
|
|
120
132
|
|
|
121
133
|
raise ArgumentError, "profile must be one of #{PROFILES.map(&:inspect).join(", ")}, got #{profile.inspect}"
|
|
122
134
|
end
|
|
135
|
+
|
|
136
|
+
# Validate +gvl+ against GVL_MODES. Like +profile+ it has no +nil+
|
|
137
|
+
# form: the scheduling mode is requested as an explicit Symbol, so
|
|
138
|
+
# anything off the set — +nil+ included — is rejected.
|
|
139
|
+
def normalize_gvl(gvl)
|
|
140
|
+
return gvl if GVL_MODES.include?(gvl)
|
|
141
|
+
|
|
142
|
+
raise ArgumentError, "gvl must be one of #{GVL_MODES.map(&:inspect).join(", ")}, got #{gvl.inspect}"
|
|
143
|
+
end
|
|
123
144
|
end
|
|
124
145
|
end
|
|
@@ -13,7 +13,8 @@ module Kobako
|
|
|
13
13
|
module Transport
|
|
14
14
|
# Pure-function dispatcher for guest-initiated transport calls.
|
|
15
15
|
# Decodes a msgpack-encoded Request envelope, resolves the target
|
|
16
|
-
# object through the
|
|
16
|
+
# object through the per-invocation path +resolver+ (the +Context+, whose
|
|
17
|
+
# +#lookup+ layers per-invocation providers over the static bindings) or
|
|
17
18
|
# Catalog::Handles (Handle lookup), invokes the method, and returns
|
|
18
19
|
# a msgpack-encoded Response envelope.
|
|
19
20
|
#
|
|
@@ -24,7 +25,7 @@ module Kobako
|
|
|
24
25
|
#
|
|
25
26
|
# Entry point:
|
|
26
27
|
#
|
|
27
|
-
# Kobako::Transport::Dispatcher.dispatch(request_bytes,
|
|
28
|
+
# Kobako::Transport::Dispatcher.dispatch(request_bytes, resolver, handler, yield_to_guest)
|
|
28
29
|
# # => msgpack-encoded Response bytes (never raises)
|
|
29
30
|
module Dispatcher
|
|
30
31
|
# Throw tag for the Yielder's break unwind back to the
|
|
@@ -66,12 +67,12 @@ module Kobako
|
|
|
66
67
|
private_constant :CALLABLE_ALLOW
|
|
67
68
|
|
|
68
69
|
# Dispatch a single transport request and return the encoded
|
|
69
|
-
# Response bytes. Invoked from the
|
|
70
|
-
# +Kobako::
|
|
70
|
+
# Response bytes. Invoked from the per-invocation dispatch Proc that
|
|
71
|
+
# +Kobako::Context+ hands to +Runtime#eval+ / +#run+; +resolver+,
|
|
71
72
|
# +handler+, and +yield_to_guest+ are captured in that Proc's
|
|
72
|
-
# closure so the Dispatcher stays stateless and the
|
|
73
|
-
#
|
|
74
|
-
# or +Runtime+. +yield_to_guest+ is a +String → String+ callable
|
|
73
|
+
# closure so the Dispatcher stays stateless and neither the resolver
|
|
74
|
+
# nor the Context needs to publish accessors for the per-invocation
|
|
75
|
+
# +Catalog::Handles+ or +Runtime+. +yield_to_guest+ is a +String → String+ callable
|
|
75
76
|
# (the ext's per-dispatch +Kobako::Runtime::GuestYielder+) used only
|
|
76
77
|
# when the Request carries +block_given: true+. Always
|
|
77
78
|
# returns a binary String — every failure path is reified as a
|
|
@@ -81,9 +82,9 @@ module Kobako
|
|
|
81
82
|
# The decode runs inside +Codec.track_handles+ so #resolve_call_args
|
|
82
83
|
# can skip the argument walk when no Capability Handle crossed the
|
|
83
84
|
# wire.
|
|
84
|
-
def dispatch(request_bytes,
|
|
85
|
+
def dispatch(request_bytes, resolver, handler, yield_to_guest)
|
|
85
86
|
request, carried_handle = Kobako::Codec.track_handles { Kobako::Transport::Request.decode(request_bytes) }
|
|
86
|
-
target = resolve_target(request.target,
|
|
87
|
+
target = resolve_target(request.target, resolver, handler)
|
|
87
88
|
args, kwargs = resolve_call_args(request, handler, carried_handle)
|
|
88
89
|
yielder = Yielder.new(yield_to_guest, BREAK_THROW, handler) if request.block_given
|
|
89
90
|
encode_ok(catch(BREAK_THROW) { invoke(target, request.method_name, args, kwargs, yielder) }, handler)
|
|
@@ -192,24 +193,24 @@ module Kobako
|
|
|
192
193
|
raise UndefinedTargetError, e.message
|
|
193
194
|
end
|
|
194
195
|
|
|
195
|
-
# Resolve a Request target to the Ruby object the
|
|
196
|
-
# Catalog::Handles) holds. String targets go through the
|
|
196
|
+
# Resolve a Request target to the Ruby object the path +resolver+ (or
|
|
197
|
+
# Catalog::Handles) holds. String targets go through the resolver;
|
|
197
198
|
# Handle targets (ext 0x01) go through the Catalog::Handles.
|
|
198
199
|
#
|
|
199
200
|
# Target type is already validated by +Transport::Request.decode+
|
|
200
201
|
# before this method is reached, so no else-branch is needed here —
|
|
201
202
|
# the wire layer is the system boundary that enforces the invariant.
|
|
202
|
-
def resolve_target(target,
|
|
203
|
+
def resolve_target(target, resolver, handler)
|
|
203
204
|
case target
|
|
204
205
|
when String
|
|
205
|
-
resolve_path(target,
|
|
206
|
+
resolve_path(target, resolver)
|
|
206
207
|
when Kobako::Handle
|
|
207
208
|
require_live_object!(target.id, handler)
|
|
208
209
|
end
|
|
209
210
|
end
|
|
210
211
|
|
|
211
|
-
def resolve_path(path,
|
|
212
|
-
|
|
212
|
+
def resolve_path(path, resolver)
|
|
213
|
+
resolver.lookup(path)
|
|
213
214
|
rescue KeyError => e
|
|
214
215
|
raise UndefinedTargetError, e.message
|
|
215
216
|
end
|
|
@@ -47,7 +47,7 @@ module Kobako
|
|
|
47
47
|
# #invalidate!. The ok value is consumed by the host Service
|
|
48
48
|
# method, so a Capability Handle in it is restored to its host object.
|
|
49
49
|
# The break value unwinds past the Service back to the guest
|
|
50
|
-
#
|
|
50
|
+
# bound-constant call, so it passes through verbatim — a Handle stays a
|
|
51
51
|
# Handle and rides back on the same id rather than churning a new one.
|
|
52
52
|
def yield(*args)
|
|
53
53
|
raise LocalJumpError, "guest block invoked after host dispatch frame returned" unless @active
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
# Kobako::Unresolved — the sentinel backing a fillable Service path: one
|
|
5
|
+
# declared with +Sandbox#bind(path)+ carrying no object. It reserves the
|
|
6
|
+
# path's Frame 1 slot so the guest sees the bound constant while the host
|
|
7
|
+
# defers the object it stands for. A guest dispatch to an unfilled fillable
|
|
8
|
+
# is refused as an unresolved target and surfaces as +Kobako::ServiceError+
|
|
9
|
+
# when the guest leaves it unrescued — the same capability-failure channel
|
|
10
|
+
# as an idiom with no backend bound.
|
|
11
|
+
#
|
|
12
|
+
# A single shared value; only its identity distinguishes it, so the dispatch
|
|
13
|
+
# layer recognises it with +equal?+. A Host App may name it at a +bind+ site
|
|
14
|
+
# to declare a fillable explicitly.
|
|
15
|
+
module Unresolved
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/kobako/usage.rb
CHANGED
|
@@ -21,16 +21,16 @@ module Kobako
|
|
|
21
21
|
# the high-water.
|
|
22
22
|
#
|
|
23
23
|
# Both readers are populated on every outcome, including +TrapError+
|
|
24
|
-
# branches, so the Host App can read
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
24
|
+
# branches, so the Host App can read +#usage+ off the run's
|
|
25
|
+
# +Kobako::Execution+ — the one a raised error carries on +#execution+ —
|
|
26
|
+
# after rescuing a trap to diagnose how much of the budget the failing
|
|
27
|
+
# invocation consumed.
|
|
28
28
|
#
|
|
29
29
|
# Built on the +class X < Data.define(...)+ subclass form (the
|
|
30
30
|
# Steep-friendly shape — see +lib/kobako/outcome/panic.rb+).
|
|
31
31
|
class Usage < Data.define(:wall_time, :memory_peak)
|
|
32
|
-
# Pre-
|
|
33
|
-
#
|
|
32
|
+
# Pre-run sentinel. A fresh +Kobako::Context+ holds it until its guest
|
|
33
|
+
# runs, so an Execution's +#usage+ is never +nil+.
|
|
34
34
|
EMPTY = new(wall_time: 0.0, memory_peak: 0)
|
|
35
35
|
end
|
|
36
36
|
end
|
data/lib/kobako/version.rb
CHANGED
data/lib/kobako.rb
CHANGED
data/release-please-config.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
3
|
"release-type": "ruby",
|
|
4
|
+
"bump-minor-pre-major": true,
|
|
4
5
|
"last-release-sha": "98509af508988f708bf0d7a76a718bb0428a177e",
|
|
5
6
|
"packages": {
|
|
6
7
|
".": {
|
|
7
8
|
"component": "kobako",
|
|
8
9
|
"include-component-in-tag": false,
|
|
9
10
|
"release-type": "ruby",
|
|
10
|
-
"exclude-paths": ["wasm"]
|
|
11
|
+
"exclude-paths": ["wasm", "crates"]
|
|
11
12
|
},
|
|
12
13
|
"wasm/kobako-core": {
|
|
13
14
|
"component": "kobako-core",
|
|
@@ -7,21 +7,19 @@ module Kobako
|
|
|
7
7
|
|
|
8
8
|
def seal!: () -> self
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def resolve: () -> Hash[String, untyped]
|
|
11
11
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def install_object: (untyped backend) -> untyped
|
|
15
|
+
|
|
16
|
+
def resolve_backend: (untyped extension, Hash[String, untyped] resolved, Hash[untyped, untyped] by_provider) -> void
|
|
15
17
|
|
|
16
18
|
def validate!: (untyped extension) -> void
|
|
17
19
|
|
|
18
20
|
def assert_dependencies!: () -> void
|
|
19
21
|
|
|
20
22
|
def symbolize: (untyped name) -> untyped
|
|
21
|
-
|
|
22
|
-
def callable?: (untyped provider) -> bool
|
|
23
|
-
|
|
24
|
-
def initial_object: (untyped provider) -> untyped
|
|
25
23
|
end
|
|
26
24
|
end
|
|
27
25
|
end
|
|
@@ -3,13 +3,13 @@ module Kobako
|
|
|
3
3
|
class Services
|
|
4
4
|
NAME_PATTERN: Regexp
|
|
5
5
|
|
|
6
|
-
def initialize: (
|
|
6
|
+
def initialize: () -> void
|
|
7
7
|
|
|
8
8
|
def bind: (Symbol | String path, untyped object) -> self
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def bound?: (Symbol | String path) -> bool
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def lookup: (Symbol | String target) -> untyped
|
|
13
13
|
|
|
14
14
|
def encode: () -> String
|
|
15
15
|
|